From 4fc7ea95b1a130360d2f236ccb589c900024768b Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 12 Nov 2025 15:54:51 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat/#24:=20kakao=20app=20key=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20local.properties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 8 ++++++++ app/src/main/AndroidManifest.xml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 59f7d49..46db608 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,6 +31,7 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" buildConfigField("String", "BASE_URL", "\"${properties["base.url"]}\"") + manifestPlaceholders["kakaoAppKey"] = properties["kakao.native.app.key"] as String } @@ -91,4 +92,11 @@ dependencies { // KaKao implementation("com.kakao.sdk:v2-user:2.20.1") + // DataStore + implementation("androidx.datastore:datastore-preferences:1.1.1") + + // Paging3 + implementation("androidx.paging:paging-runtime-ktx:3.3.0") + implementation("androidx.paging:paging-compose:3.3.0") + } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 94bbce5..2ae4b22 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,7 +36,7 @@ - + From 0929b4ba158b030bee0491bff2a41e9b762658f8 Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 12 Nov 2025 15:55:12 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat/#24:=20token=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hsLink/hslink/data/di/DataStoreModule.kt | 21 +++++++++++++++ .../hslink/data/remote/AuthInterceptor.kt | 27 +++++++++++++++++++ .../data/repositoryimpl/AuthRepositoryImpl.kt | 9 ++++--- .../hslink/data/service/login/AuthService.kt | 4 +-- .../domain/repository/AuthRepository.kt | 2 +- 5 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/hsLink/hslink/data/di/DataStoreModule.kt create mode 100644 app/src/main/java/com/hsLink/hslink/data/remote/AuthInterceptor.kt diff --git a/app/src/main/java/com/hsLink/hslink/data/di/DataStoreModule.kt b/app/src/main/java/com/hsLink/hslink/data/di/DataStoreModule.kt new file mode 100644 index 0000000..e602220 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/di/DataStoreModule.kt @@ -0,0 +1,21 @@ +package com.hsLink.hslink.data.di + +import android.content.Context +import com.hsLink.hslink.data.local.TokenDataStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DataStoreModule { + + @Provides + @Singleton + fun provideTokenDataStore(@ApplicationContext context: Context): TokenDataStore { + return TokenDataStore(context) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/remote/AuthInterceptor.kt b/app/src/main/java/com/hsLink/hslink/data/remote/AuthInterceptor.kt new file mode 100644 index 0000000..a8d3380 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/remote/AuthInterceptor.kt @@ -0,0 +1,27 @@ +package com.hsLink.hslink.data.remote + +import com.hsLink.hslink.data.local.TokenDataStore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject + +class AuthInterceptor @Inject constructor( + private val tokenDataStore: TokenDataStore +) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val accessToken = runBlocking { + tokenDataStore.accessToken.first() + } + + val requestBuilder = chain.request().newBuilder() + + if (!accessToken.isNullOrBlank()) { + requestBuilder.addHeader("Authorization", "Bearer $accessToken") + } + + return chain.proceed(requestBuilder.build()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/AuthRepositoryImpl.kt b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/AuthRepositoryImpl.kt index 3a499ec..308347e 100644 --- a/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/AuthRepositoryImpl.kt @@ -1,13 +1,15 @@ package com.hsLink.hslink.data.repositoryimpl -import com.hsLink.hslink.data.dto.request.SocialLoginRequestDto -import com.hsLink.hslink.data.dto.response.SocialLoginResponseDto +import com.hsLink.hslink.data.dto.request.auth.SocialLoginRequestDto +import com.hsLink.hslink.data.dto.response.auth.SocialLoginResponseDto +import com.hsLink.hslink.data.local.TokenDataStore import com.hsLink.hslink.data.service.login.AuthService import com.hsLink.hslink.domain.repository.AuthRepository import javax.inject.Inject class AuthRepositoryImpl @Inject constructor( - private val authService: AuthService + private val authService: AuthService, + private val tokenDataStore: TokenDataStore ) : AuthRepository { override suspend fun loginWithSocialToken( @@ -20,6 +22,7 @@ class AuthRepositoryImpl @Inject constructor( if (response.isSuccessful) { response.body()?.let { loginResponse -> + tokenDataStore.saveTokens(loginResponse.accessToken, loginResponse.refreshToken) Result.success(loginResponse) } ?: Result.failure(Exception("응답이 비어있습니다")) } else { diff --git a/app/src/main/java/com/hsLink/hslink/data/service/login/AuthService.kt b/app/src/main/java/com/hsLink/hslink/data/service/login/AuthService.kt index 6b63311..84be672 100644 --- a/app/src/main/java/com/hsLink/hslink/data/service/login/AuthService.kt +++ b/app/src/main/java/com/hsLink/hslink/data/service/login/AuthService.kt @@ -1,7 +1,7 @@ package com.hsLink.hslink.data.service.login -import com.hsLink.hslink.data.dto.request.SocialLoginRequestDto -import com.hsLink.hslink.data.dto.response.SocialLoginResponseDto +import com.hsLink.hslink.data.dto.request.auth.SocialLoginRequestDto +import com.hsLink.hslink.data.dto.response.auth.SocialLoginResponseDto import retrofit2.Response import retrofit2.http.Body import retrofit2.http.POST diff --git a/app/src/main/java/com/hsLink/hslink/domain/repository/AuthRepository.kt b/app/src/main/java/com/hsLink/hslink/domain/repository/AuthRepository.kt index acc0dab..b32597a 100644 --- a/app/src/main/java/com/hsLink/hslink/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/hsLink/hslink/domain/repository/AuthRepository.kt @@ -1,6 +1,6 @@ package com.hsLink.hslink.domain.repository -import com.hsLink.hslink.data.dto.response.SocialLoginResponseDto +import com.hsLink.hslink.data.dto.response.auth.SocialLoginResponseDto interface AuthRepository { suspend fun loginWithSocialToken( From c5e344d4a6685e1db577026dc37f794d56dc9805 Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 12 Nov 2025 15:55:56 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat/#24:=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => auth}/SocialLoginRequestDto.kt | 2 +- .../{ => auth}/SocialLoginResponseDto.kt | 2 +- .../{ => mypage}/UserProfileResponseDto.kt | 2 +- .../{ => search}/SearchResponseDto.kt | 2 +- .../hslink/data/local/TokenDataStore.kt | 42 +++++++++++++++++++ .../remote/datasource/SearchDataSource.kt | 4 +- .../datasourceimpl/SearchDataSourceImpl.kt | 4 +- .../data/service/search/SearchService.kt | 4 +- 8 files changed, 52 insertions(+), 10 deletions(-) rename app/src/main/java/com/hsLink/hslink/data/dto/request/{ => auth}/SocialLoginRequestDto.kt (75%) rename app/src/main/java/com/hsLink/hslink/data/dto/response/{ => auth}/SocialLoginResponseDto.kt (81%) rename app/src/main/java/com/hsLink/hslink/data/dto/response/{ => mypage}/UserProfileResponseDto.kt (94%) rename app/src/main/java/com/hsLink/hslink/data/dto/response/{ => search}/SearchResponseDto.kt (91%) create mode 100644 app/src/main/java/com/hsLink/hslink/data/local/TokenDataStore.kt diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/request/SocialLoginRequestDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/request/auth/SocialLoginRequestDto.kt similarity index 75% rename from app/src/main/java/com/hsLink/hslink/data/dto/request/SocialLoginRequestDto.kt rename to app/src/main/java/com/hsLink/hslink/data/dto/request/auth/SocialLoginRequestDto.kt index fe22c4e..88200fc 100644 --- a/app/src/main/java/com/hsLink/hslink/data/dto/request/SocialLoginRequestDto.kt +++ b/app/src/main/java/com/hsLink/hslink/data/dto/request/auth/SocialLoginRequestDto.kt @@ -1,4 +1,4 @@ -package com.hsLink.hslink.data.dto.request +package com.hsLink.hslink.data.dto.request.auth import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/SocialLoginResponseDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/auth/SocialLoginResponseDto.kt similarity index 81% rename from app/src/main/java/com/hsLink/hslink/data/dto/response/SocialLoginResponseDto.kt rename to app/src/main/java/com/hsLink/hslink/data/dto/response/auth/SocialLoginResponseDto.kt index b1ef367..2904d42 100644 --- a/app/src/main/java/com/hsLink/hslink/data/dto/response/SocialLoginResponseDto.kt +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/auth/SocialLoginResponseDto.kt @@ -1,4 +1,4 @@ -package com.hsLink.hslink.data.dto.response +package com.hsLink.hslink.data.dto.response.auth import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/UserProfileResponseDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/mypage/UserProfileResponseDto.kt similarity index 94% rename from app/src/main/java/com/hsLink/hslink/data/dto/response/UserProfileResponseDto.kt rename to app/src/main/java/com/hsLink/hslink/data/dto/response/mypage/UserProfileResponseDto.kt index 6ebf15c..64dbcc1 100644 --- a/app/src/main/java/com/hsLink/hslink/data/dto/response/UserProfileResponseDto.kt +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/mypage/UserProfileResponseDto.kt @@ -1,4 +1,4 @@ -package com.hsLink.hslink.data.dto.response +package com.hsLink.hslink.data.dto.response.mypage import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/SearchResponseDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/search/SearchResponseDto.kt similarity index 91% rename from app/src/main/java/com/hsLink/hslink/data/dto/response/SearchResponseDto.kt rename to app/src/main/java/com/hsLink/hslink/data/dto/response/search/SearchResponseDto.kt index b5c6e88..b1f0de1 100644 --- a/app/src/main/java/com/hsLink/hslink/data/dto/response/SearchResponseDto.kt +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/search/SearchResponseDto.kt @@ -1,4 +1,4 @@ -package com.hsLink.hslink.data.dto.response +package com.hsLink.hslink.data.dto.response.search import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/hsLink/hslink/data/local/TokenDataStore.kt b/app/src/main/java/com/hsLink/hslink/data/local/TokenDataStore.kt new file mode 100644 index 0000000..d1698d8 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/local/TokenDataStore.kt @@ -0,0 +1,42 @@ +package com.hsLink.hslink.data.local + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class TokenDataStore @Inject constructor(private val context: Context) { + + private val Context.dataStore: DataStore by preferencesDataStore(name = "hsu_connect_tokens") + + private val accessTokenKey = stringPreferencesKey("access_token") + private val refreshTokenKey = stringPreferencesKey("refresh_token") + + val accessToken: Flow = context.dataStore.data + .map { preferences -> + preferences[accessTokenKey] + } + + val refreshToken: Flow = context.dataStore.data + .map { preferences -> + preferences[refreshTokenKey] + } + + suspend fun saveTokens(accessToken: String, refreshToken: String) { + context.dataStore.edit { + it[accessTokenKey] = accessToken + it[refreshTokenKey] = refreshToken + } + } + + suspend fun clearTokens() { + context.dataStore.edit { + it.clear() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/remote/datasource/SearchDataSource.kt b/app/src/main/java/com/hsLink/hslink/data/remote/datasource/SearchDataSource.kt index 9737b0d..9861bb5 100644 --- a/app/src/main/java/com/hsLink/hslink/data/remote/datasource/SearchDataSource.kt +++ b/app/src/main/java/com/hsLink/hslink/data/remote/datasource/SearchDataSource.kt @@ -1,7 +1,7 @@ package com.hsLink.hslink.data.remote.datasource -import com.hsLink.hslink.data.dto.response.MentorListResponseDto -import com.hsLink.hslink.data.dto.response.UserProfileResponseDto +import com.hsLink.hslink.data.dto.response.search.MentorListResponseDto +import com.hsLink.hslink.data.dto.response.mypage.UserProfileResponseDto interface SearchDataSource { suspend fun getMentors(page: Int, size: Int): MentorListResponseDto diff --git a/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/SearchDataSourceImpl.kt b/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/SearchDataSourceImpl.kt index e105d13..fcfbe58 100644 --- a/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/SearchDataSourceImpl.kt +++ b/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/SearchDataSourceImpl.kt @@ -1,7 +1,7 @@ package com.hsLink.hslink.data.remote.datasourceimpl -import com.hsLink.hslink.data.dto.response.MentorListResponseDto -import com.hsLink.hslink.data.dto.response.UserProfileResponseDto +import com.hsLink.hslink.data.dto.response.search.MentorListResponseDto +import com.hsLink.hslink.data.dto.response.mypage.UserProfileResponseDto import com.hsLink.hslink.data.remote.datasource.SearchDataSource import com.hsLink.hslink.data.service.search.SearchService import javax.inject.Inject diff --git a/app/src/main/java/com/hsLink/hslink/data/service/search/SearchService.kt b/app/src/main/java/com/hsLink/hslink/data/service/search/SearchService.kt index 39e83df..7191340 100644 --- a/app/src/main/java/com/hsLink/hslink/data/service/search/SearchService.kt +++ b/app/src/main/java/com/hsLink/hslink/data/service/search/SearchService.kt @@ -1,7 +1,7 @@ package com.hsLink.hslink.data.service.search -import com.hsLink.hslink.data.dto.response.MentorListResponseDto -import com.hsLink.hslink.data.dto.response.UserProfileResponseDto +import com.hsLink.hslink.data.dto.response.search.MentorListResponseDto +import com.hsLink.hslink.data.dto.response.mypage.UserProfileResponseDto import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query From 66f12d7d4a56caf7fd86dd0e981995a9a9b1ea0e Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 12 Nov 2025 15:56:16 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat/#24:=20post=20api=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(=ED=86=A0=ED=81=B0=EC=B6=94=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hslink/data/dto/request/{ => community}/PostRequestDto.kt | 4 ++-- .../hslink/data/dto/response/{ => home}/PostResponseDto.kt | 2 +- .../hsLink/hslink/data/remote/datasource/PostDataSource.kt | 4 ++-- .../hslink/data/remote/datasourceimpl/PostDataSourceImpl.kt | 4 ++-- .../hslink/data/repositoryimpl/home/PostRepositoryImpl.kt | 2 +- .../java/com/hsLink/hslink/data/service/home/PostService.kt | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) rename app/src/main/java/com/hsLink/hslink/data/dto/request/{ => community}/PostRequestDto.kt (83%) rename app/src/main/java/com/hsLink/hslink/data/dto/response/{ => home}/PostResponseDto.kt (96%) diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/request/PostRequestDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/request/community/PostRequestDto.kt similarity index 83% rename from app/src/main/java/com/hsLink/hslink/data/dto/request/PostRequestDto.kt rename to app/src/main/java/com/hsLink/hslink/data/dto/request/community/PostRequestDto.kt index b861d1e..efac9a9 100644 --- a/app/src/main/java/com/hsLink/hslink/data/dto/request/PostRequestDto.kt +++ b/app/src/main/java/com/hsLink/hslink/data/dto/request/community/PostRequestDto.kt @@ -1,4 +1,4 @@ -package com.hsLink.hslink.data.dto.request +package com.hsLink.hslink.data.dto.request.community import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -11,4 +11,4 @@ data class PostRequestDto ( val title : String, @SerialName("body") val body : String -) +) \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/PostResponseDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/home/PostResponseDto.kt similarity index 96% rename from app/src/main/java/com/hsLink/hslink/data/dto/response/PostResponseDto.kt rename to app/src/main/java/com/hsLink/hslink/data/dto/response/home/PostResponseDto.kt index e422bdc..c51fe5a 100644 --- a/app/src/main/java/com/hsLink/hslink/data/dto/response/PostResponseDto.kt +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/home/PostResponseDto.kt @@ -1,4 +1,4 @@ -package com.hsLink.hslink.data.dto.response +package com.hsLink.hslink.data.dto.response.home import com.hsLink.hslink.domain.model.home.PostPopularEntity import com.hsLink.hslink.domain.model.home.PostPromotionEntity diff --git a/app/src/main/java/com/hsLink/hslink/data/remote/datasource/PostDataSource.kt b/app/src/main/java/com/hsLink/hslink/data/remote/datasource/PostDataSource.kt index 02d2dbd..923e81b 100644 --- a/app/src/main/java/com/hsLink/hslink/data/remote/datasource/PostDataSource.kt +++ b/app/src/main/java/com/hsLink/hslink/data/remote/datasource/PostDataSource.kt @@ -1,8 +1,8 @@ package com.hsLink.hslink.data.remote.datasource import com.hsLink.hslink.core.network.BaseResponse -import com.hsLink.hslink.data.dto.response.PostPromotionDto -import com.hsLink.hslink.data.dto.response.PostResponseDto +import com.hsLink.hslink.data.dto.response.home.PostPromotionDto +import com.hsLink.hslink.data.dto.response.home.PostResponseDto interface PostDataSource { suspend fun getPopularPost() : BaseResponse diff --git a/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/PostDataSourceImpl.kt b/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/PostDataSourceImpl.kt index 45835c4..03b8699 100644 --- a/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/PostDataSourceImpl.kt +++ b/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/PostDataSourceImpl.kt @@ -1,8 +1,8 @@ package com.hsLink.hslink.data.remote.datasourceimpl import com.hsLink.hslink.core.network.BaseResponse -import com.hsLink.hslink.data.dto.response.PostPromotionDto -import com.hsLink.hslink.data.dto.response.PostResponseDto +import com.hsLink.hslink.data.dto.response.home.PostPromotionDto +import com.hsLink.hslink.data.dto.response.home.PostResponseDto import com.hsLink.hslink.data.remote.datasource.PostDataSource import com.hsLink.hslink.data.service.home.PostService import javax.inject.Inject diff --git a/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/home/PostRepositoryImpl.kt b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/home/PostRepositoryImpl.kt index 41ad244..12e2ed1 100644 --- a/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/home/PostRepositoryImpl.kt +++ b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/home/PostRepositoryImpl.kt @@ -1,6 +1,6 @@ package com.hsLink.hslink.data.repositoryimpl.home -import com.hsLink.hslink.data.dto.response.toEntity +import com.hsLink.hslink.data.dto.response.home.toEntity import com.hsLink.hslink.data.remote.datasourceimpl.PostDataSourceImpl import com.hsLink.hslink.domain.model.home.PostPopularEntity import com.hsLink.hslink.domain.model.home.PostPromotionEntity diff --git a/app/src/main/java/com/hsLink/hslink/data/service/home/PostService.kt b/app/src/main/java/com/hsLink/hslink/data/service/home/PostService.kt index 86681f0..5a2c369 100644 --- a/app/src/main/java/com/hsLink/hslink/data/service/home/PostService.kt +++ b/app/src/main/java/com/hsLink/hslink/data/service/home/PostService.kt @@ -1,8 +1,8 @@ package com.hsLink.hslink.data.service.home import com.hsLink.hslink.core.network.BaseResponse -import com.hsLink.hslink.data.dto.response.PostPromotionDto -import com.hsLink.hslink.data.dto.response.PostResponseDto +import com.hsLink.hslink.data.dto.response.home.PostPromotionDto +import com.hsLink.hslink.data.dto.response.home.PostResponseDto import retrofit2.http.GET From 8f8e3e355f6c2c99480cda3708fa85954ece7110 Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 12 Nov 2025 15:56:40 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat/#24:=20post=20api=20(=EA=B8=80?= =?UTF-8?q?=EC=93=B0=EA=B8=B0,=20=EB=A6=AC=EC=8A=A4=ED=8A=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommunityPostResponseDto.kt | 2 +- .../datasource/CommunityPostDataSource.kt | 4 +-- .../CommunityPostDataSourceImpl.kt | 7 ++-- .../repositoryimpl/CommunityRepositoryImpl.kt | 34 +++++++++++++------ .../commuunity/CommunityPostService.kt | 16 ++++++--- .../community/CommunityRepository.kt | 9 +++-- 6 files changed, 48 insertions(+), 24 deletions(-) rename app/src/main/java/com/hsLink/hslink/data/dto/response/{ => community}/CommunityPostResponseDto.kt (88%) diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/CommunityPostResponseDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/community/CommunityPostResponseDto.kt similarity index 88% rename from app/src/main/java/com/hsLink/hslink/data/dto/response/CommunityPostResponseDto.kt rename to app/src/main/java/com/hsLink/hslink/data/dto/response/community/CommunityPostResponseDto.kt index 3238825..798d23d 100644 --- a/app/src/main/java/com/hsLink/hslink/data/dto/response/CommunityPostResponseDto.kt +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/community/CommunityPostResponseDto.kt @@ -1,4 +1,4 @@ -package com.hsLink.hslink.data.dto.response +package com.hsLink.hslink.data.dto.response.community import com.hsLink.hslink.domain.model.community.CommunityPostResponseEntity import kotlinx.serialization.SerialName diff --git a/app/src/main/java/com/hsLink/hslink/data/remote/datasource/CommunityPostDataSource.kt b/app/src/main/java/com/hsLink/hslink/data/remote/datasource/CommunityPostDataSource.kt index 6784ca2..e403e1c 100644 --- a/app/src/main/java/com/hsLink/hslink/data/remote/datasource/CommunityPostDataSource.kt +++ b/app/src/main/java/com/hsLink/hslink/data/remote/datasource/CommunityPostDataSource.kt @@ -1,8 +1,8 @@ package com.hsLink.hslink.data.remote.datasource import com.hsLink.hslink.core.network.BaseResponse -import com.hsLink.hslink.data.dto.request.PostRequestDto -import com.hsLink.hslink.data.dto.response.CommunityPostResponseDto +import com.hsLink.hslink.data.dto.request.community.PostRequestDto +import com.hsLink.hslink.data.dto.response.community.CommunityPostResponseDto interface CommunityPostDataSource { suspend fun createCommunityPost( diff --git a/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/CommunityPostDataSourceImpl.kt b/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/CommunityPostDataSourceImpl.kt index cb2ec8f..b54c10b 100644 --- a/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/CommunityPostDataSourceImpl.kt +++ b/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/CommunityPostDataSourceImpl.kt @@ -1,8 +1,8 @@ package com.hsLink.hslink.data.remote.datasourceimpl import com.hsLink.hslink.core.network.BaseResponse -import com.hsLink.hslink.data.dto.request.PostRequestDto -import com.hsLink.hslink.data.dto.response.CommunityPostResponseDto +import com.hsLink.hslink.data.dto.request.community.PostRequestDto +import com.hsLink.hslink.data.dto.response.community.CommunityPostResponseDto import com.hsLink.hslink.data.remote.datasource.CommunityPostDataSource import com.hsLink.hslink.data.service.commuunity.CommunityPostService import javax.inject.Inject @@ -13,7 +13,6 @@ class CommunityPostDataSourceImpl @Inject constructor( override suspend fun createCommunityPost( request : PostRequestDto ) : BaseResponse { - // TODO: Replace dummy token with real access token - return communityPostService.postCommunity(token = "", requestBody = request) + return communityPostService.postCommunity(requestBody = request) } } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/CommunityRepositoryImpl.kt b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/CommunityRepositoryImpl.kt index f1d16d2..e6a7f1e 100644 --- a/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/CommunityRepositoryImpl.kt +++ b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/CommunityRepositoryImpl.kt @@ -1,20 +1,34 @@ package com.hsLink.hslink.data.repositoryimpl -import com.hsLink.hslink.data.dto.request.PostRequestDto -import com.hsLink.hslink.data.dto.response.CommunityPostResponseDto -import com.hsLink.hslink.data.remote.datasourceimpl.CommunityPostDataSourceImpl +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.hsLink.hslink.data.dto.request.community.PostRequestDto +import com.hsLink.hslink.data.dto.response.community.CommunityPostResponseDto +import com.hsLink.hslink.data.paging.CommunityPagingSource +import com.hsLink.hslink.data.service.commuunity.CommunityPostService +import com.hsLink.hslink.domain.model.community.CommunityPost import com.hsLink.hslink.domain.repository.community.CommunityRepository +import kotlinx.coroutines.flow.Flow import javax.inject.Inject class CommunityRepositoryImpl @Inject constructor( - private val communityPostDataSourceImpl: CommunityPostDataSourceImpl, + private val communityPostService: CommunityPostService, ) : CommunityRepository { - override suspend fun createCommunityPost(communityRequestDto: PostRequestDto): Result = runCatching { - val response = communityPostDataSourceImpl.createCommunityPost(communityRequestDto) - if (response.isSuccess) { - response.result - } else { - throw Exception(response.message) + override suspend fun createCommunityPost(communityRequestDto: PostRequestDto): Result = + runCatching { + val response = communityPostService.postCommunity(communityRequestDto) + if (response.isSuccess) { + response.result + } else { + throw Exception(response.message) + } } + + override fun getCommunityPosts(type: String): Flow> { + return Pager( + config = PagingConfig(pageSize = 20), + pagingSourceFactory = { CommunityPagingSource(communityPostService, type) } + ).flow } } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/service/commuunity/CommunityPostService.kt b/app/src/main/java/com/hsLink/hslink/data/service/commuunity/CommunityPostService.kt index 7868e67..2068cfb 100644 --- a/app/src/main/java/com/hsLink/hslink/data/service/commuunity/CommunityPostService.kt +++ b/app/src/main/java/com/hsLink/hslink/data/service/commuunity/CommunityPostService.kt @@ -1,17 +1,23 @@ package com.hsLink.hslink.data.service.commuunity import com.hsLink.hslink.core.network.BaseResponse -import com.hsLink.hslink.data.dto.request.PostRequestDto -import com.hsLink.hslink.data.dto.response.CommunityPostResponseDto +import com.hsLink.hslink.data.dto.request.community.PostRequestDto +import com.hsLink.hslink.data.dto.response.community.CommunityListResponseDto +import com.hsLink.hslink.data.dto.response.community.CommunityPostResponseDto import retrofit2.http.Body -import retrofit2.http.Header +import retrofit2.http.GET import retrofit2.http.POST - +import retrofit2.http.Query interface CommunityPostService { @POST("posts") suspend fun postCommunity( - @Header("Authorization") token: String, @Body requestBody: PostRequestDto, ): BaseResponse + + @GET("posts") + suspend fun getCommunity( + @Query("type") type: String, + @Query("page") page: Int, + ): BaseResponse } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/domain/repository/community/CommunityRepository.kt b/app/src/main/java/com/hsLink/hslink/domain/repository/community/CommunityRepository.kt index 0a08789..48addc9 100644 --- a/app/src/main/java/com/hsLink/hslink/domain/repository/community/CommunityRepository.kt +++ b/app/src/main/java/com/hsLink/hslink/domain/repository/community/CommunityRepository.kt @@ -1,8 +1,13 @@ package com.hsLink.hslink.domain.repository.community -import com.hsLink.hslink.data.dto.request.PostRequestDto -import com.hsLink.hslink.data.dto.response.CommunityPostResponseDto +import androidx.paging.PagingData +import com.hsLink.hslink.data.dto.request.community.PostRequestDto +import com.hsLink.hslink.data.dto.response.community.CommunityPostResponseDto +import com.hsLink.hslink.domain.model.community.CommunityPost +import kotlinx.coroutines.flow.Flow interface CommunityRepository { suspend fun createCommunityPost(communityRequestDto: PostRequestDto): Result + + fun getCommunityPosts(type: String): Flow> } From deef364cb025b23f6b885b3693d7d249364b9f6d Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 12 Nov 2025 15:56:53 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat/#24:=20navigation=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/caches/deviceStreaming.xml | 324 +++++++++++++++++- .../hsLink/hslink/data/di/NetworkModule.kt | 61 ++-- .../navigation/main/Communitynavigation.kt | 12 +- .../hslink/presentation/main/MainNavHost.kt | 10 +- 4 files changed, 348 insertions(+), 59 deletions(-) diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml index e5f298b..37bf1cf 100644 --- a/.idea/caches/deviceStreaming.xml +++ b/.idea/caches/deviceStreaming.xml @@ -51,18 +51,6 @@ diff --git a/app/src/main/java/com/hsLink/hslink/data/di/NetworkModule.kt b/app/src/main/java/com/hsLink/hslink/data/di/NetworkModule.kt index d434a4b..36ed472 100644 --- a/app/src/main/java/com/hsLink/hslink/data/di/NetworkModule.kt +++ b/app/src/main/java/com/hsLink/hslink/data/di/NetworkModule.kt @@ -1,6 +1,7 @@ package com.hsLink.hslink.data.di import com.hsLink.hslink.BuildConfig +import com.hsLink.hslink.data.remote.AuthInterceptor import com.hsLink.hslink.data.service.commuunity.CommunityPostService import com.hsLink.hslink.data.service.home.PostService import com.hsLink.hslink.data.service.login.AuthService @@ -13,9 +14,7 @@ import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Converter import retrofit2.Retrofit -import java.util.concurrent.TimeUnit import javax.inject.Singleton @Module @@ -24,54 +23,46 @@ object NetworkModule { @Provides @Singleton - fun providesLoggingInterceptor() = HttpLoggingInterceptor().apply { - level = if (BuildConfig.DEBUG) { - HttpLoggingInterceptor.Level.BODY - } else { - HttpLoggingInterceptor.Level.NONE - } + fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(authInterceptor) + .addInterceptor( + HttpLoggingInterceptor().apply { + level = if (BuildConfig.DEBUG) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.NONE + } + } + ) + .build() } @Provides @Singleton - fun providesOkHttpClient( - loggingInterceptor: HttpLoggingInterceptor, - ): OkHttpClient = OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .addInterceptor(loggingInterceptor) - .build() - - @Provides - @Singleton - fun providesConverterFactory(): Converter.Factory = - Json.Default.asConverterFactory("application/json".toMediaType()) - - @Provides - @Singleton - fun providesRetrofit( - client: OkHttpClient, - converterFactory: Converter.Factory, - ): Retrofit { + fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { return Retrofit.Builder() .baseUrl(BuildConfig.BASE_URL) - .addConverterFactory(converterFactory) - .client(client) + .client(okHttpClient) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) .build() } @Provides @Singleton - fun providePostService(retrofit: Retrofit): PostService = - retrofit.create(PostService::class.java) + fun provideCommunityPostService(retrofit: Retrofit): CommunityPostService { + return retrofit.create(CommunityPostService::class.java) + } @Provides @Singleton - fun provideAuthService(retrofit: Retrofit): AuthService { - return retrofit.create(AuthService::class.java) + fun providePostService(retrofit: Retrofit): PostService { + return retrofit.create(PostService::class.java) } + @Provides @Singleton - fun provideCommunityPostService(retrofit: Retrofit): CommunityPostService = - retrofit.create(CommunityPostService::class.java) + fun provideAuthService(retrofit: Retrofit): AuthService { + return retrofit.create(AuthService::class.java) + } } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/community/navigation/main/Communitynavigation.kt b/app/src/main/java/com/hsLink/hslink/presentation/community/navigation/main/Communitynavigation.kt index bdcbe0f..7888c84 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/community/navigation/main/Communitynavigation.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/community/navigation/main/Communitynavigation.kt @@ -15,16 +15,14 @@ fun NavController.navigateToCommunity(navOptions: NavOptions? = null) { fun NavGraphBuilder.communityNavGraph( padding: PaddingValues, - navigateUp : () -> Unit, - navigateToWriting : () -> Unit, - navigateToPost: (String) -> Unit, + navigateToWriteCommunity : () -> Unit, + navigateToPost: (Int) -> Unit, ) { composable { CommunityRoute( - padding, - navigateUp = navigateUp, - navigateWriteCommunity = navigateToWriting, - navigateToPost = navigateToPost + paddingValues = padding, + navigateWriteCommunity = navigateToWriteCommunity, + navigateToPost = navigateToPost, ) } } diff --git a/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavHost.kt b/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavHost.kt index 0acad41..39122cd 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavHost.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavHost.kt @@ -49,11 +49,11 @@ fun MainNavHost( ) communityNavGraph( - padding, - navigateUp = navigator::navigateUp, - navigateToWriting = navigator::navigateWriteCommunity, - navigateToPost = navigator::navigateToCommunityPost + padding = padding, + navigateToWriteCommunity = navigator::navigateWriteCommunity, + navigateToPost = { postId -> navigator.navigateToCommunityPost(postId.toString()) }, ) + mypageNavGraph( padding = padding, navController = navigator.navController @@ -86,4 +86,4 @@ fun MainNavHost( ) } -} \ No newline at end of file +} From 019c86b8f64dc4207a29aee369c807c2bcc80cc0 Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 12 Nov 2025 15:57:07 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat/#24:=20paging=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20=EB=B0=8F=20screen=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../community/CommunityListResponseDto.kt | 32 +++++++++++ .../data/paging/CommunityPagingSource.kt | 36 ++++++++++++ .../domain/model/community/CommunityPost.kt | 23 ++++++++ .../component/main/CommunityCardItem.kt | 57 ++++++++++++++++--- 4 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/hsLink/hslink/data/dto/response/community/CommunityListResponseDto.kt create mode 100644 app/src/main/java/com/hsLink/hslink/data/paging/CommunityPagingSource.kt create mode 100644 app/src/main/java/com/hsLink/hslink/domain/model/community/CommunityPost.kt diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/community/CommunityListResponseDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/community/CommunityListResponseDto.kt new file mode 100644 index 0000000..ea4ba4d --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/community/CommunityListResponseDto.kt @@ -0,0 +1,32 @@ +package com.hsLink.hslink.data.dto.response.community + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CommunityListResponseDto( + @SerialName("posts") + val posts: List, + @SerialName("currentPage") + val currentPage: Int, + @SerialName("hasNext") + val hasNext: Boolean, + @SerialName("totalElements") + val totalElements: Int, +) + +@Serializable +data class CommunityListDto( + @SerialName("id") + val id: Int, + @SerialName("title") + val title: String, + @SerialName("summary") + val summary: String, + @SerialName("author") + val author: String, + @SerialName("studentId") + val studentId: String, + @SerialName("authorStatus") + val authorStatus: String, +) diff --git a/app/src/main/java/com/hsLink/hslink/data/paging/CommunityPagingSource.kt b/app/src/main/java/com/hsLink/hslink/data/paging/CommunityPagingSource.kt new file mode 100644 index 0000000..1fe690b --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/paging/CommunityPagingSource.kt @@ -0,0 +1,36 @@ +package com.hsLink.hslink.data.paging + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.hsLink.hslink.data.service.commuunity.CommunityPostService +import com.hsLink.hslink.domain.model.community.CommunityPost +import com.hsLink.hslink.domain.model.community.toEntity + +class CommunityPagingSource( + private val communityPostService: CommunityPostService, + private val type: String +) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: 0 + + return try { + val response = communityPostService.getCommunity(type = type, page = page) + val posts = response.result.posts.map { it.toEntity() } + + LoadResult.Page( + data = posts, + prevKey = if (page == 0) null else page - 1, + nextKey = if (response.result.hasNext) page + 1 else null + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { + state.closestPageToPosition(it)?.prevKey?.plus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/domain/model/community/CommunityPost.kt b/app/src/main/java/com/hsLink/hslink/domain/model/community/CommunityPost.kt new file mode 100644 index 0000000..f0f9f93 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/domain/model/community/CommunityPost.kt @@ -0,0 +1,23 @@ +package com.hsLink.hslink.domain.model.community + +import com.hsLink.hslink.data.dto.response.community.CommunityListDto + +data class CommunityPost( + val id: Int, + val title: String, + val summary: String, + val author: String, + val studentId: String, + val authorStatus: String, +) + +fun CommunityListDto.toEntity(): CommunityPost { + return CommunityPost( + id = id, + title = title, + summary = summary, + author = author, + studentId = studentId, + authorStatus = authorStatus, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/community/component/main/CommunityCardItem.kt b/app/src/main/java/com/hsLink/hslink/presentation/community/component/main/CommunityCardItem.kt index 49693d1..33a3133 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/community/component/main/CommunityCardItem.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/community/component/main/CommunityCardItem.kt @@ -30,6 +30,8 @@ private fun PreviewCommunityCardItem() { userName = "John Doe", userMajor = "Computer Science", userInfo = "Senior at XYZ University", + userId = "12345", + author = "Author Name", onClick = {}, ) } @@ -40,6 +42,8 @@ fun CommunityCardItem( userName: String, userMajor: String, userInfo: String, + userId:String, + author:String, onClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -75,13 +79,52 @@ fun CommunityCardItem( overflow = TextOverflow.Ellipsis ) - Text( - text = userInfo, - color = HsLinkTheme.colors.Grey400, - style = HsLinkTheme.typography.caption_12Normal, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically + ){ + Text( + text = author, + color = HsLinkTheme.colors.Grey400, + style = HsLinkTheme.typography.caption_12Normal, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = "·", + color = HsLinkTheme.colors.Grey700, + style = HsLinkTheme.typography.title_16Strong, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = userId, + color = HsLinkTheme.colors.Grey400, + style = HsLinkTheme.typography.caption_12Normal, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = "·", + color = HsLinkTheme.colors.Grey700, + style = HsLinkTheme.typography.title_16Strong, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = userInfo, + color = HsLinkTheme.colors.Grey400, + style = HsLinkTheme.typography.caption_12Normal, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + } } } } \ No newline at end of file From 3c16800bf39b3d51ddfb3747c408b7f550b160af Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 12 Nov 2025 15:57:18 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat/#24:=20community=20api=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../community/screen/main/CommunityScreen.kt | 173 ++++++++++-------- .../screen/write/CommunityWritingScreen.kt | 15 +- .../community/viewmodel/CommunityViewModel.kt | 29 ++- 3 files changed, 125 insertions(+), 92 deletions(-) diff --git a/app/src/main/java/com/hsLink/hslink/presentation/community/screen/main/CommunityScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/community/screen/main/CommunityScreen.kt index aec5f31..895fcf1 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/community/screen/main/CommunityScreen.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/community/screen/main/CommunityScreen.kt @@ -11,19 +11,18 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow @@ -32,93 +31,60 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource + import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems import com.hsLink.hslink.R import com.hsLink.hslink.core.designsystem.component.HsLinkTopBar import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme import com.hsLink.hslink.core.util.noRippleClickable +import com.hsLink.hslink.domain.model.community.CommunityPost import com.hsLink.hslink.presentation.community.component.CommunityTab import com.hsLink.hslink.presentation.community.component.CommunityTabLayout +import com.hsLink.hslink.presentation.community.viewmodel.CommunityViewModel import com.hsLink.hslink.presentation.home.component.CommunityCardItem -data class CommunityPost( - val id: String, - val userName: String, - val userMajor: String, - val userInfo: String, -) - @Preview(showBackground = true) @Composable private fun PreviewCommunityScreen() { HsLinkTheme { - CommunityScreen( - paddingValues = PaddingValues(), - navigateUp = {}, - navigateWriteCommunity = {}, - onClick = {}, - navigateToPost = {} - ) } } @Composable fun CommunityRoute( paddingValues: PaddingValues, - navigateUp: () -> Unit, navigateWriteCommunity: () -> Unit, - navigateToPost: (String) -> Unit, + navigateToPost: (Int) -> Unit, + viewModel: CommunityViewModel = hiltViewModel(), ) { + val selectedTab by viewModel.selectedTab.collectAsState(initial = CommunityTab.Popular) + val communityPosts = viewModel.communityPosts.collectAsLazyPagingItems() + CommunityScreen( paddingValues = paddingValues, - navigateUp = navigateUp, navigateWriteCommunity = navigateWriteCommunity, - onClick = {}, - navigateToPost = navigateToPost + navigateToPost = navigateToPost, + selectedTab = selectedTab, + onTabSelected = viewModel::selectTab, + posts = communityPosts ) } @Composable fun CommunityScreen( paddingValues: PaddingValues, - navigateUp: () -> Unit, navigateWriteCommunity: () -> Unit, - onClick: () -> Unit, - navigateToPost: (String) -> Unit, + navigateToPost: (Int) -> Unit, modifier: Modifier = Modifier, - - ) { - var selectedTab by remember { mutableStateOf(CommunityTab.Popular) } - - val posts = remember(selectedTab) { - when (selectedTab) { - CommunityTab.Popular -> listOf( - CommunityPost("1", "인기글 작성자1", "컴퓨터공학과", "인기글 내용입니다"), - CommunityPost("2", "인기글 작성자2", "경영학과", "좋아요가 많은 글"), - CommunityPost("3", "인기글 작성자3", "디자인학과", "핫한 글입니다"), - ) - - CommunityTab.Free -> listOf( - CommunityPost("4", "자유 작성자1", "전자공학과", "자유게시판 글1"), - CommunityPost("5", "자유 작성자2", "수학과", "자유게시판 글2"), - CommunityPost("6", "자유 작성자3", "물리학과", "자유게시판 글3"), - ) - - CommunityTab.Promotion -> listOf( - CommunityPost("7", "홍보 작성자1", "마케팅학과", "동아리 홍보합니다"), - CommunityPost("8", "홍보 작성자2", "광고홍보학과", "행사 알림"), - CommunityPost("9", "홍보 작성자3", "경제학과", "스터디 모집"), - ) - - CommunityTab.Notice -> listOf( - CommunityPost("10", "관리자1", "학생처", "중요 공지사항"), - CommunityPost("11", "관리자2", "교무처", "학사 일정 안내"), - CommunityPost("12", "관리자3", "총학생회", "필독 공지"), - ) - } - } - + selectedTab: CommunityTab, + onTabSelected: (CommunityTab) -> Unit, + posts: LazyPagingItems, +) { Column( modifier = modifier .fillMaxSize() @@ -145,34 +111,79 @@ fun CommunityScreen( CommunityTabLayout( selectedTab = selectedTab, - onTabSelected = { tab -> - selectedTab = tab - } + onTabSelected = onTabSelected ) Box( modifier = Modifier.fillMaxHeight() ) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues( - start = 16.dp, - end = 16.dp, - top = 16.dp, - bottom = 80.dp - ), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - items( - items = posts, - key = { it.id } - ) { post -> - CommunityCardItem( - userName = post.userName, - userMajor = post.userMajor, - userInfo = post.userInfo, - onClick = { navigateToPost(post.id) } - ) + when (posts.loadState.refresh) { + is LoadState.Loading -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + + is LoadState.Error -> { + val error = posts.loadState.refresh as LoadState.Error + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = "데이터 로드 오류 발생: ${error.error.localizedMessage}", + color = Color.Red + ) + } + } + + else -> { + if (posts.itemCount == 0) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = "게시글이 존재하지 않습니다.", color = HsLinkTheme.colors.Grey500) + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + start = 16.dp, + end = 16.dp, + top = 16.dp, + bottom = 80.dp + ), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items( + count = posts.itemCount, + key = { index -> posts[index]?.id ?: index } + ) { index -> + val post = posts[index] + post?.let { + CommunityCardItem( + userName = it.title, + userMajor = it.summary, + userId = it.studentId, + userInfo = it.authorStatus, + author = it.author, + onClick = { navigateToPost(it.id) } + ) + } + } + + if (posts.loadState.append is LoadState.Loading) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } + } + } } } diff --git a/app/src/main/java/com/hsLink/hslink/presentation/community/screen/write/CommunityWritingScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/community/screen/write/CommunityWritingScreen.kt index 32e7275..4f883ab 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/community/screen/write/CommunityWritingScreen.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/community/screen/write/CommunityWritingScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.hsLink.hslink.R import com.hsLink.hslink.core.designsystem.component.HsLinkDialog import com.hsLink.hslink.core.designsystem.component.HsLinkTextField @@ -32,6 +33,7 @@ import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme import com.hsLink.hslink.presentation.community.component.BoardSelectionField import com.hsLink.hslink.presentation.community.component.BoardType import com.hsLink.hslink.presentation.community.component.CommunityWriteButton +import com.hsLink.hslink.presentation.community.viewmodel.CommunityViewModel @Preview(showBackground = true) @Composable @@ -40,7 +42,8 @@ private fun CommunityWritingScreenPreview() { CommunityWritingScreen( paddingValues = PaddingValues(), navigateUp = {}, - navigateToCommunity = {} + navigateToCommunity = {}, + createPost = { _, _, _ -> } ) } } @@ -50,11 +53,13 @@ fun CommunityWritingRoute( paddingValues: PaddingValues, navigateUp: () -> Unit, navigateToCommunity: () -> Unit, + viewModel: CommunityViewModel = hiltViewModel() ) { CommunityWritingScreen( paddingValues = paddingValues, navigateUp = navigateUp, - navigateToCommunity = navigateToCommunity + navigateToCommunity = navigateToCommunity, + createPost = viewModel::createPost ) } @@ -63,6 +68,7 @@ fun CommunityWritingScreen( paddingValues: PaddingValues, navigateUp: () -> Unit, navigateToCommunity: () -> Unit, + createPost: (String, String, String) -> Unit, modifier: Modifier = Modifier, ) { var selectedBoardType by remember { mutableStateOf(null) } @@ -261,6 +267,9 @@ fun CommunityWritingScreen( confirmText = "업로드하기", dismissText = "취소하기", onConfirm = { + selectedBoardType?.let { boardType -> + createPost(boardType.name, title, content) + } showUploadDialog = false navigateToCommunity() }, @@ -269,4 +278,4 @@ fun CommunityWritingScreen( } ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hsLink/hslink/presentation/community/viewmodel/CommunityViewModel.kt b/app/src/main/java/com/hsLink/hslink/presentation/community/viewmodel/CommunityViewModel.kt index efe1b57..b8a2d2f 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/community/viewmodel/CommunityViewModel.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/community/viewmodel/CommunityViewModel.kt @@ -2,24 +2,37 @@ package com.hsLink.hslink.presentation.community.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.hsLink.hslink.data.dto.request.PostRequestDto +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.hsLink.hslink.data.dto.request.community.PostRequestDto +import com.hsLink.hslink.domain.model.community.CommunityPost import com.hsLink.hslink.domain.repository.community.CommunityRepository -import com.hsLink.hslink.presentation.community.state.CommunityContract +import com.hsLink.hslink.presentation.community.component.CommunityTab import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class CommunityViewModel @Inject constructor( private val repository: CommunityRepository -) : ViewModel(){ +) : ViewModel() { + + private val _selectedTab = MutableStateFlow(CommunityTab.Popular) + val selectedTab: Flow = _selectedTab + + val communityPosts: Flow> = _selectedTab + .flatMapLatest { tab -> + repository.getCommunityPosts(tab.name.lowercase()) + } + .cachedIn(viewModelScope) + + fun selectTab(tab: CommunityTab) { + _selectedTab.value = tab + } - private val _state = MutableStateFlow(CommunityContract()) - val state: StateFlow = _state.asStateFlow() - fun createPost(postType: String, title: String, body: String) { viewModelScope.launch { val request = PostRequestDto( From ec21d5f94fc3fffb08f7ff4b38f41c7b46b9284e Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 12 Nov 2025 16:51:50 +0900 Subject: [PATCH 09/11] feat/#24: CI/CD --- app/build.gradle.kts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 46db608..0bbfe68 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,5 @@ import java.util.Properties +import org.gradle.api.GradleException plugins { alias(libs.plugins.android.application) @@ -17,6 +18,21 @@ val properties = Properties().apply { load(localPropertiesFile.inputStream()) } } + +fun getRequiredProperty(key: String): String { + + val localValue = properties[key]?.toString() + if (!localValue.isNullOrBlank()) { + return localValue + } + + val envValue = System.getenv(key.toUpperCase().replace('.', '_')) + if (!envValue.isNullOrBlank()) { + return envValue + } + + throw GradleException("Property '$key' is missing. Please define it in local.properties or as an environment variable (e.g., ${key.toUpperCase().replace('.', '_')}) in CI/CD.") +} android { namespace = "com.hsLink.hslink" compileSdk = libs.versions.compileSdk.get().toInt() @@ -30,8 +46,8 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - buildConfigField("String", "BASE_URL", "\"${properties["base.url"]}\"") - manifestPlaceholders["kakaoAppKey"] = properties["kakao.native.app.key"] as String + buildConfigField("String", "BASE_URL", "\"${getRequiredProperty("base.url")}\"") + manifestPlaceholders["kakaoAppKey"] = getRequiredProperty("kakao.native.app.key") } @@ -42,7 +58,7 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) - buildConfigField("String", "BASE_URL", "\"${properties["base.url"]}\"") + buildConfigField("String", "BASE_URL", "\"${getRequiredProperty("base.url")}\"") } } compileOptions { From 27e6f408ebf1346ff9cde323776925eee84e4c1a Mon Sep 17 00:00:00 2001 From: Son Juwan <113279387+vvan2@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:03:32 +0900 Subject: [PATCH 10/11] Add environment variables to CI/CD workflow Added environment variables for BASE_URL and KAKAO_NATIVE_APP_KEY in Gradle and Fastlane steps. --- .github/workflows/ci-cd.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 9067f16..f808962 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -35,6 +35,9 @@ jobs: - name: Build with Gradle run: ./gradlew assembleDebug + env: + BASE_URL: ${{ secrets.BASE_URL }} + KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} - name: Run unit tests run: ./gradlew test @@ -87,4 +90,7 @@ jobs: run: chmod +x gradlew - name: Run Fastlane distribute - run: bundle exec fastlane distribute \ No newline at end of file + run: bundle exec fastlane distribute + env: + BASE_URL: ${{ secrets.BASE_URL }} + KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} From 368ae515f929458dc99c784bd5eeaec99243df18 Mon Sep 17 00:00:00 2001 From: Son Juwan <113279387+vvan2@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:27:37 +0900 Subject: [PATCH 11/11] Add environment variables for unit tests --- .github/workflows/ci-cd.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index f808962..3d9c95a 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -41,6 +41,9 @@ jobs: - name: Run unit tests run: ./gradlew test + env: +          BASE_URL: ${{ secrets.BASE_URL }} +          KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} - name: Upload test reports if: always()