From 40224bc5259812f61f9d7ac5e61354abe3e97e73 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:39:40 +0900 Subject: [PATCH 01/14] =?UTF-8?q?[feat]:=20=EB=82=B4=20=EB=8F=99=EC=95=84?= =?UTF-8?q?=EB=A6=AC/=ED=95=99=EA=B3=BC=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20data=20layer=20=EC=83=9D=EC=84=B1=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/remote/RemoteClubDataSource.kt | 26 ++++++++++++++++++- .../client/data/repository/ClubRepository.kt | 5 +++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt index c7f75ca..190ab1f 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt @@ -1,9 +1,33 @@ package org.whosin.client.data.remote import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.http.isSuccess +import org.whosin.client.core.network.ApiResult +import org.whosin.client.data.dto.response.MyClubResponseDto class RemoteClubDataSource( - private val client : HttpClient + private val client: HttpClient ) { + suspend fun getMyClubs(): ApiResult { + return try { + val response: HttpResponse = client.get(urlString = "clubs/my") + if (response.status.isSuccess()) { + ApiResult.Success( + data = response.body(), + statusCode = response.status.value + ) + } else { + ApiResult.Error( + code = response.status.value, + message = "HTTP Error: ${response.status.value}" + ) + } + } catch (t: Throwable) { + ApiResult.Error(message = t.message, cause = t) + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt index 79a7830..ea4d6a3 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt @@ -1,9 +1,12 @@ package org.whosin.client.data.repository +import org.whosin.client.core.network.ApiResult +import org.whosin.client.data.dto.response.MyClubResponseDto import org.whosin.client.data.remote.RemoteClubDataSource class ClubRepository( private val dataSource: RemoteClubDataSource ) { - + suspend fun getMyClubs(): ApiResult = + dataSource.getMyClubs() } \ No newline at end of file From 565bf9bf07491d9f4f06f7eb46fd0594d34986d1 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:40:15 +0900 Subject: [PATCH 02/14] =?UTF-8?q?[feat]:=20=EB=82=B4=20=EB=8F=99=EC=95=84?= =?UTF-8?q?=EB=A6=AC/=ED=95=99=EA=B3=BC=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20viewmodel=20=EC=9E=91=EC=84=B1=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/presentation/home/HomeViewModel.kt | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeViewModel.kt index cdf5121..0ea0f22 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeViewModel.kt @@ -1,10 +1,60 @@ package org.whosin.client.presentation.home import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.whosin.client.core.network.ApiResult import org.whosin.client.data.repository.ClubRepository +data class ClubUi( + val id: Int, + val name: String +) + +data class HomeUiState( + val isClubsLoading: Boolean = true, + val isMembersLoading: Boolean = false, + val clubs: List = emptyList(), + val selectedClub: ClubUi? = null, + val isAttending: Boolean = false, + val errorMessage: String? = null +) + class HomeViewModel( - private val repository: ClubRepository + private val clubRepository: ClubRepository ): ViewModel() { + private val _uiState = MutableStateFlow(HomeUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadMyClubs() + } + + private fun loadMyClubs() { + viewModelScope.launch { + _uiState.update { it.copy(isClubsLoading = true) } + when (val result = clubRepository.getMyClubs()) { + is ApiResult.Success -> { + val clubs = result.data.data.userClubs.map { ClubUi(it.clubId, it.clubName) } + _uiState.update { it.copy(isClubsLoading = false, clubs = clubs) } + // 첫 번째 클럽을 자동으로 선택하고 멤버 목록 로드 + clubs.firstOrNull()?.let { firstClub -> + onClubSelected(firstClub) + } + } + is ApiResult.Error -> { + _uiState.update { it.copy(isClubsLoading = false, errorMessage = "동아리 목록을 불러오지 못했습니다.") } + } + } + } + } + fun onClubSelected(club: ClubUi) { + _uiState.update { it.copy(selectedClub = club) } +// loadPresentMembers(club.id) + } } \ No newline at end of file From 15b05f17e48784794029c1b8924fb2447792b2c1 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:41:02 +0900 Subject: [PATCH 03/14] =?UTF-8?q?[feat]:=20=EB=82=B4=20=EB=8F=99=EC=95=84?= =?UTF-8?q?=EB=A6=AC/=ED=95=99=EA=B3=BC=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=97=B0=EA=B2=B0=20=EB=A1=9C=EC=A7=81=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/presentation/home/HomeScreen.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt index 7f3142e..369d516 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt @@ -40,11 +40,13 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import kotlinx.coroutines.launch import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel import org.whosin.client.presentation.home.component.MyClubSidebar import org.whosin.client.presentation.home.component.PresentMembersList import org.whosin.client.presentation.home.mock.sampleUsers @@ -61,14 +63,13 @@ fun HomeScreen( modifier: Modifier = Modifier, onNavigateBack: () -> Unit, onNavigateToMyPage: () -> Unit, + viewModel: HomeViewModel = koinViewModel() ) { val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() var isAttending by remember { mutableStateOf(true) } - val clubs = listOf("메이커스팜", "목방", "건대교지편집위원회", "국어국문학과", "컴퓨터공학부") - - var selectedClub by remember { mutableStateOf(clubs.first()) } ModalNavigationDrawer( drawerState = drawerState, @@ -81,10 +82,13 @@ fun HomeScreen( ) { MyClubSidebar( modifier = Modifier.height(325.dp), - clubs = clubs, - selectedClub = selectedClub, - onClubSelected = { newClub -> - selectedClub = newClub + clubs = uiState.clubs.map { it.name }, + selectedClub = uiState.selectedClub?.name ?: "", + onClubSelected = { clubName -> + // 이름으로 다시 ClubUi 객체 찾기 + uiState.clubs.find { it.name == clubName }?.let { + viewModel.onClubSelected(it) + } }, onClose = { scope.launch { drawerState.close() } @@ -140,7 +144,7 @@ fun HomeScreen( Column { Text( - text = stringResource(Res.string.current_whos_in_top, selectedClub), + text = stringResource(Res.string.current_whos_in_top, uiState.selectedClub?.name ?: "..."), color = Color.Black, fontSize = 20.sp, lineHeight = 32.sp, From 08bee90fac4841bffdf4d0924d1fb11a1a7dc193 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:41:31 +0900 Subject: [PATCH 04/14] =?UTF-8?q?[feat]:=20header=EC=97=90=20token=20?= =?UTF-8?q?=EB=84=A3=EA=B8=B0=20(=ED=95=98=EB=93=9C=EC=BD=94=EB=94=A9)=20(?= =?UTF-8?q?#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/core/network/HttpClientFactory.kt | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/core/network/HttpClientFactory.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/core/network/HttpClientFactory.kt index d1627b5..2191610 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/core/network/HttpClientFactory.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/core/network/HttpClientFactory.kt @@ -15,6 +15,7 @@ import io.ktor.client.plugins.logging.Logging import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.ContentType +import io.ktor.http.Url import io.ktor.http.contentType import io.ktor.http.encodedPath import io.ktor.serialization.kotlinx.json.json @@ -47,31 +48,42 @@ object HttpClientFactory { install(Auth){ bearer { loadTokens { - val accessToken = tokenManager.getAccessToken() ?: "no_token" + val accessToken = tokenManager.getAccessToken() ?: "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJ1c2VySWQiOjUsInByb3ZpZGVySWQiOiJsb2NhbGhvc3QiLCJuYW1lIjoi7Iug7KKF7JykIiwicm9sZSI6IlJPTEVfTUVNQkVSIiwiaWF0IjoxNzU5MzgyMzg3LCJleHAiOjE3NTk5ODcxODd9.kT9IH60aCA-6ByEITb-_qPAJY0Oik1bbPKqcBWXzHIk" val refreshToken = tokenManager.getRefreshToken() ?: "no_token" BearerTokens(accessToken = accessToken, refreshToken = refreshToken) } sendWithoutRequest { request -> - val host = "https://"+request.url.host+"/" - val path = request.url.encodedPath - val pathWithNoAuth = listOf( - "jokes", - "users/signup", - "users/find-password", - "auth/login", - "auth/email", - "auth/email/validation" - ) - // 결과가 true면 Authorization 헤더 추가, false면 제거 - if(host != BASE_URL){ + val requestHost = request.url.host + val baseHost = try { + Url(BASE_URL).host + } catch (e: Exception) { + // BASE_URL 형식이 잘못되었을 경우를 대비한 예외 처리 + null + } + + // 요청하는 API의 host가 우리 서버의 host와 다르면 외부 API로 간주하여 토큰을 보내지 않음 + if (requestHost != baseHost) { println("External API - No Auth") false - }else{ - // pathWithNoAuth에 있는 경로에는 Authorization 헤더 제외 + } else { + // 우리 서버로 요청하는 경우, 인증이 필요 없는 경로인지 확인 + val path = request.url.encodedPath + val pathWithNoAuth = listOf( + "jokes", + "users/signup", + "users/find-password", + "auth/login", + "auth/email", + "auth/email/validation", + "member/reissue" // 토큰 재발급 요청 자체에는 만료된 액세스 토큰을 보내면 안 됨 + ) + val isNoAuthPath = pathWithNoAuth.any { noAuthPath -> - path.startsWith(noAuthPath) || path.contains(noAuthPath) + path.contains(noAuthPath) } - println("isNoAuthPath: $isNoAuthPath") + + // isNoAuthPath가 true이면 인증이 필요 없는 경로 -> 헤더를 보내지 않음 (false 반환) + // isNoAuthPath가 false이면 인증이 필요한 경로 -> 헤더를 보냄 (true 반환) !isNoAuthPath } } From 6b342baf789e17b1495391a4fd6803b698ddaf1b Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:41:44 +0900 Subject: [PATCH 05/14] =?UTF-8?q?[feat]:=20=EC=95=88=EB=93=9C=EB=A1=9C?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=EC=97=90=EC=84=9C=20http=20=ED=97=88?= =?UTF-8?q?=EC=9A=A9=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composeApp/src/androidMain/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 6c3b28f..2ea8450 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -10,7 +10,8 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@android:style/Theme.Material.Light.NoActionBar"> + android:theme="@android:style/Theme.Material.Light.NoActionBar" + android:usesCleartextTraffic="true"> From 918fe9b949663a55b7c8374a7e23978839a97701 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:41:49 +0900 Subject: [PATCH 06/14] =?UTF-8?q?[feat]:=20ios=EC=97=90=EC=84=9C=20http=20?= =?UTF-8?q?=ED=97=88=EC=9A=A9=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iosApp/iosApp.xcodeproj/project.pbxproj | 178 ++++++++++++------------ iosApp/iosApp/Info.plist | 14 ++ 2 files changed, 103 insertions(+), 89 deletions(-) diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 7df713e..2e09c07 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -21,6 +21,11 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + B7F09170141D1DFD01FA8F16 /* Configuration */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Configuration; + sourceTree = ""; + }; D20DF483BC423C080236AF15 /* iosApp */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -29,11 +34,6 @@ path = iosApp; sourceTree = ""; }; - B7F09170141D1DFD01FA8F16 /* Configuration */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = Configuration; - sourceTree = ""; - }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -98,7 +98,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1620; - LastUpgradeCheck = 1620; + LastUpgradeCheck = 2600; TargetAttributes = { 59D23463E6199566C42AAE4E = { CreatedOnToolsVersion = 16.2; @@ -167,7 +167,59 @@ /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ - 9B78CB08AB61AA6668CFCC6D /* Debug */ = { + 48BE1DB7B4F1B09BF56AB6D4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 803A8C7FA01596D81A28696E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 88E820AB40C74C08BA206107 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReferenceAnchor = B7F09170141D1DFD01FA8F16 /* Configuration */; baseConfigurationReferenceRelativePath = Config.xcconfig; @@ -203,18 +255,13 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = YP5KFGY422; + ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -223,16 +270,16 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; }; - name = Debug; + name = Release; }; - 88E820AB40C74C08BA206107 /* Release */ = { + 9B78CB08AB61AA6668CFCC6D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReferenceAnchor = B7F09170141D1DFD01FA8F16 /* Configuration */; baseConfigurationReferenceRelativePath = Config.xcconfig; @@ -268,12 +315,19 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = YP5KFGY422; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -282,87 +336,33 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 48BE1DB7B4F1B09BF56AB6D4 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ARCHS = arm64; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = "${TEAM_ID}"; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = iosApp/Info.plist; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; - 803A8C7FA01596D81A28696E /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ARCHS = arm64; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = "${TEAM_ID}"; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = iosApp/Info.plist; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - C5C7529C8178E941499526D2 /* Build configuration list for PBXProject "iosApp" */ = { + A6202DAD4BF832B61214D26A /* Build configuration list for PBXNativeTarget "iosApp" */ = { isa = XCConfigurationList; buildConfigurations = ( - 9B78CB08AB61AA6668CFCC6D /* Debug */, - 88E820AB40C74C08BA206107 /* Release */, + 48BE1DB7B4F1B09BF56AB6D4 /* Debug */, + 803A8C7FA01596D81A28696E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - A6202DAD4BF832B61214D26A /* Build configuration list for PBXNativeTarget "iosApp" */ = { + C5C7529C8178E941499526D2 /* Build configuration list for PBXProject "iosApp" */ = { isa = XCConfigurationList; buildConfigurations = ( - 48BE1DB7B4F1B09BF56AB6D4 /* Debug */, - 803A8C7FA01596D81A28696E /* Release */, + 9B78CB08AB61AA6668CFCC6D /* Debug */, + 88E820AB40C74C08BA206107 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -370,4 +370,4 @@ /* End XCConfigurationList section */ }; rootObject = FE6262850FD121756379321F /* Project object */; -} \ No newline at end of file +} diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index 11845e1..46a8995 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -4,5 +4,19 @@ CADisableMinimumFrameDurationOnPhone + + NSAppTransportSecurity + + NSExceptionDomains + + whosinroom.store + + NSIncludesSubdomains + + NSExceptionAllowsInsecureHTTPLoads + + + + From 3bb10c1551e227ce645981f738aa219d53f2f788 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:04:15 +0900 Subject: [PATCH 07/14] =?UTF-8?q?[feat]:=20=EC=9E=AC=EC=8B=A4=EC=9E=90=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20data=20layer=20=EC=83=9D=EC=84=B1=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/ClubPresencesResponseDto.kt | 32 +++++++++++++++++++ .../data/remote/RemoteClubDataSource.kt | 21 ++++++++++++ .../client/data/repository/ClubRepository.kt | 4 +++ 3 files changed, 57 insertions(+) create mode 100644 composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ClubPresencesResponseDto.kt diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ClubPresencesResponseDto.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ClubPresencesResponseDto.kt new file mode 100644 index 0000000..875fe8b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ClubPresencesResponseDto.kt @@ -0,0 +1,32 @@ +package org.whosin.client.data.dto.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ClubPresencesResponseDto( + @SerialName("success") + val success: Boolean, + @SerialName("status") + val status: Int, + @SerialName("message") + val message: String, + @SerialName("data") + val data: ClubPresencesData +) + +@Serializable +data class ClubPresencesData( + @SerialName("clubName") + val clubName: String, + @SerialName("presentMembers") + val presentMembers: List +) + +@Serializable +data class PresentMembers( + @SerialName("userName") + val userName: String, + @SerialName("isMe") + val isMe: Boolean +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt index 190ab1f..4b6fc9b 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt @@ -6,6 +6,7 @@ import io.ktor.client.request.get import io.ktor.client.statement.HttpResponse import io.ktor.http.isSuccess import org.whosin.client.core.network.ApiResult +import org.whosin.client.data.dto.response.ClubPresencesResponseDto import org.whosin.client.data.dto.response.MyClubResponseDto class RemoteClubDataSource( @@ -30,4 +31,24 @@ class RemoteClubDataSource( ApiResult.Error(message = t.message, cause = t) } } + + suspend fun getPresentMembers(clubId: Int): ApiResult { + return try { + val response: HttpResponse = client.get(urlString = "clubs/$clubId/presences") + + if (response.status.isSuccess()) { + ApiResult.Success( + data = response.body(), + statusCode = response.status.value + ) + } else { + ApiResult.Error( + code = response.status.value, + message = "HTTP Error: ${response.status.value}" + ) + } + } catch (t: Throwable) { + ApiResult.Error(message = t.message, cause = t) + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt index ea4d6a3..f1f9d4d 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt @@ -1,6 +1,7 @@ package org.whosin.client.data.repository import org.whosin.client.core.network.ApiResult +import org.whosin.client.data.dto.response.ClubPresencesResponseDto import org.whosin.client.data.dto.response.MyClubResponseDto import org.whosin.client.data.remote.RemoteClubDataSource @@ -9,4 +10,7 @@ class ClubRepository( ) { suspend fun getMyClubs(): ApiResult = dataSource.getMyClubs() + + suspend fun getPresentMembers(clubId: Int): ApiResult = + dataSource.getPresentMembers(clubId) } \ No newline at end of file From 45c1620e242e570b0e0e0bfd17dc36f96f6ffec5 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:04:55 +0900 Subject: [PATCH 08/14] =?UTF-8?q?[feat]:=20=EC=9E=AC=EC=8B=A4=EC=9E=90=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20viewmodel=20=EC=9E=91=EC=84=B1=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/presentation/home/HomeViewModel.kt | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeViewModel.kt index 0ea0f22..22232f4 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeViewModel.kt @@ -15,11 +15,18 @@ data class ClubUi( val name: String ) +data class PresentMemberUi( + val userName: String, + val isMe: Boolean +) + data class HomeUiState( val isClubsLoading: Boolean = true, val isMembersLoading: Boolean = false, val clubs: List = emptyList(), val selectedClub: ClubUi? = null, + val selectedClubName: String? = null, + val presentMembers: List = emptyList(), val isAttending: Boolean = false, val errorMessage: String? = null ) @@ -54,7 +61,47 @@ class HomeViewModel( } fun onClubSelected(club: ClubUi) { - _uiState.update { it.copy(selectedClub = club) } -// loadPresentMembers(club.id) + _uiState.update { it.copy(selectedClub = club, presentMembers = emptyList()) } // 클럽 변경 시 멤버 목록 초기화 + loadPresentMembers(club.id) + } + + fun refresh() { + _uiState.value.selectedClub?.id?.let { + loadPresentMembers(it) + } + } + + private fun loadPresentMembers(clubId: Int) { + viewModelScope.launch { + _uiState.update { it.copy(isMembersLoading = true, errorMessage = null) } + + when (val result = clubRepository.getPresentMembers(clubId)) { + is ApiResult.Success -> { + val responseData = result.data.data + val membersUi = responseData.presentMembers.map { dto -> + PresentMemberUi(userName = dto.userName, isMe = dto.isMe) + } + + val amIAttending = membersUi.any { it.isMe } + + _uiState.update { + it.copy( + isMembersLoading = false, + selectedClubName = responseData.clubName, + presentMembers = membersUi, + isAttending = amIAttending + ) + } + } + is ApiResult.Error -> { + _uiState.update { + it.copy( + isMembersLoading = false, + errorMessage = result.message ?: "재실자 명단을 불러오지 못했습니다." + ) + } + } + } + } } } \ No newline at end of file From 0d2f7fe6d16de9217d98569f4bdd93de6c192d7c Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:05:42 +0900 Subject: [PATCH 09/14] =?UTF-8?q?[feat]:=20=EC=9E=AC=EC=8B=A4=EC=9E=90=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=97=B0=EA=B2=B0=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/presentation/home/HomeScreen.kt | 41 ++++++++++++------- .../home/component/PresentMembersList.kt | 7 ++-- .../presentation/home/mock/PresentMember.kt | 27 ++++-------- 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt index 369d516..e750b3b 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DrawerValue import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalNavigationDrawer @@ -61,7 +62,6 @@ import whosinclient.composeapp.generated.resources.people_count @Composable fun HomeScreen( modifier: Modifier = Modifier, - onNavigateBack: () -> Unit, onNavigateToMyPage: () -> Unit, viewModel: HomeViewModel = koinViewModel() ) { @@ -69,8 +69,6 @@ fun HomeScreen( val scope = rememberCoroutineScope() val uiState by viewModel.uiState.collectAsStateWithLifecycle() - var isAttending by remember { mutableStateOf(true) } - ModalNavigationDrawer( drawerState = drawerState, scrimColor = Color(0x66000000), @@ -89,6 +87,7 @@ fun HomeScreen( uiState.clubs.find { it.name == clubName }?.let { viewModel.onClubSelected(it) } + scope.launch { drawerState.close() } }, onClose = { scope.launch { drawerState.close() } @@ -172,7 +171,7 @@ fun HomeScreen( modifier = Modifier .padding(start = 4.dp, bottom = 3.dp) .size(24.dp) -// .clickable(onClick = onNavigateToMyPage) + .clickable { viewModel.refresh() } ) } } @@ -182,13 +181,17 @@ fun HomeScreen( verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.Center ) { - Text( - text = sampleUsers.size.toString(), // TODO: 데이터 적용 - color = Color.Black, - fontSize = 45.sp, - lineHeight = 67.5.sp, - fontWeight = FontWeight(700) - ) + if (uiState.isClubsLoading || uiState.isMembersLoading) { + CircularProgressIndicator(modifier = Modifier.size(45.dp)) + } else { + Text( + text = uiState.presentMembers.size.toString(), + color = Color.Black, + fontSize = 45.sp, + lineHeight = 67.5.sp, + fontWeight = FontWeight(700) + ) + } Text( text = stringResource(Res.string.people_count), @@ -217,19 +220,28 @@ fun HomeScreen( .padding(top = 20.dp, start = 16.dp, end = 16.dp, bottom = 118.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - PresentMembersList(presentMemberList = sampleUsers) + when { + uiState.errorMessage != null -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(text = uiState.errorMessage!!) + } + } + else -> { + PresentMembersList(presentMemberList = uiState.presentMembers) + } + } } } AnimatedContent( - targetState = isAttending, + targetState = uiState.isAttending, modifier = Modifier .align(Alignment.BottomCenter) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null // 클릭 효과 제거 ) { - isAttending = !isAttending +// viewModel.toggleAttendance() // TODO: api 연결 }, transitionSpec = { fadeIn(animationSpec = tween(1000)) togetherWith @@ -258,7 +270,6 @@ fun HomeScreen( fun HomeScreenPreview() { HomeScreen( modifier = Modifier.fillMaxSize(), - onNavigateBack = {}, onNavigateToMyPage = {} ) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/component/PresentMembersList.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/component/PresentMembersList.kt index 4d891a5..09a67e7 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/component/PresentMembersList.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/component/PresentMembersList.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow 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 @@ -23,14 +22,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview -import org.whosin.client.presentation.home.mock.PresentMember +import org.whosin.client.presentation.home.PresentMemberUi import org.whosin.client.presentation.home.mock.sampleUsers import whosinclient.composeapp.generated.resources.Res import whosinclient.composeapp.generated.resources.current_empty @Composable fun PresentMembersList( - presentMemberList: List = listOf(), + presentMemberList: List = listOf(), modifier: Modifier = Modifier, ) { val scrollState = rememberScrollState() @@ -59,7 +58,7 @@ fun PresentMembersList( verticalArrangement = Arrangement.spacedBy(10.dp) ) { presentMemberList.forEach { member -> - PresentMembersItem(presentMemberNickName = member.nickname, isMe = member.isMe) + PresentMembersItem(presentMemberNickName = member.userName, isMe = member.isMe) } } Spacer(Modifier.height(20.dp)) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/mock/PresentMember.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/mock/PresentMember.kt index 17e6355..1d126e9 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/mock/PresentMember.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/mock/PresentMember.kt @@ -1,23 +1,12 @@ package org.whosin.client.presentation.home.mock -data class PresentMember( - val nickname: String, - val isMe: Boolean = false, -) - +import org.whosin.client.presentation.home.PresentMemberUi val sampleUsers = listOf( - PresentMember("김나은", isMe = true), - PresentMember("김윤서"), - PresentMember("신종윤"), - PresentMember("조규빈"), - PresentMember("조익성"), - PresentMember("채민지"), - PresentMember("현재우"), - PresentMember("김나은1", isMe = true), - PresentMember("김윤서"), - PresentMember("신종윤2"), - PresentMember("조규빈12"), - PresentMember("조익성123"), - PresentMember("채민지"), - PresentMember("현재우1"), + PresentMemberUi("김나은", isMe = true), + PresentMemberUi("김윤서", isMe = false), + PresentMemberUi("신종윤", isMe = false), + PresentMemberUi("조규빈", isMe = false), + PresentMemberUi("조익성", isMe = false), + PresentMemberUi("채민지", isMe = false), + PresentMemberUi("현재우", isMe = false), ) From b38a1ab049527c1e4398d09360b5f6a672cca0d5 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:14:32 +0900 Subject: [PATCH 10/14] =?UTF-8?q?[feat]:=20=EC=B6=9C=EA=B7=BC/=ED=87=B4?= =?UTF-8?q?=EA=B7=BC=ED=95=98=EA=B8=B0=20data=20layer=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/remote/RemoteClubDataSource.kt | 42 +++++++++++++++++++ .../client/data/repository/ClubRepository.kt | 6 +++ 2 files changed, 48 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt index 4b6fc9b..7cb797b 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt @@ -2,7 +2,9 @@ package org.whosin.client.data.remote import io.ktor.client.HttpClient import io.ktor.client.call.body +import io.ktor.client.request.delete import io.ktor.client.request.get +import io.ktor.client.request.post import io.ktor.client.statement.HttpResponse import io.ktor.http.isSuccess import org.whosin.client.core.network.ApiResult @@ -51,4 +53,44 @@ class RemoteClubDataSource( ApiResult.Error(message = t.message, cause = t) } } + + suspend fun checkIn(clubId: Int): ApiResult { + return try { + val response: HttpResponse = client.post(urlString = "clubs/$clubId/check-in") + + if (response.status.isSuccess()) { + ApiResult.Success( + data = response.body(), + statusCode = response.status.value + ) + } else { + ApiResult.Error( + code = response.status.value, + message = "HTTP Error: ${response.status.value}" + ) + } + } catch (t: Throwable) { + ApiResult.Error(message = t.message, cause = t) + } + } + + suspend fun checkOut(clubId: Int): ApiResult { + return try { + val response: HttpResponse = client.delete(urlString = "clubs/$clubId/check-out") + + if (response.status.isSuccess()) { + ApiResult.Success( + data = response.body(), + statusCode = response.status.value + ) + } else { + ApiResult.Error( + code = response.status.value, + message = "HTTP Error: ${response.status.value}" + ) + } + } catch (t: Throwable) { + ApiResult.Error(message = t.message, cause = t) + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt index f1f9d4d..a11850c 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt @@ -13,4 +13,10 @@ class ClubRepository( suspend fun getPresentMembers(clubId: Int): ApiResult = dataSource.getPresentMembers(clubId) + + suspend fun checkIn(clubId: Int): ApiResult = + dataSource.checkIn(clubId) + + suspend fun checkOut(clubId: Int): ApiResult = + dataSource.checkOut(clubId) } \ No newline at end of file From e2cae8d6a20576af9588175f60b03fd55f1835ad Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:14:45 +0900 Subject: [PATCH 11/14] =?UTF-8?q?[feat]:=20=EC=B6=9C=EA=B7=BC/=ED=87=B4?= =?UTF-8?q?=EA=B7=BC=ED=95=98=EA=B8=B0=20viewmodel=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/presentation/home/HomeViewModel.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeViewModel.kt index 22232f4..f94a340 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeViewModel.kt @@ -23,6 +23,7 @@ data class PresentMemberUi( data class HomeUiState( val isClubsLoading: Boolean = true, val isMembersLoading: Boolean = false, + val isToggleLoading: Boolean = false, val clubs: List = emptyList(), val selectedClub: ClubUi? = null, val selectedClubName: String? = null, @@ -104,4 +105,37 @@ class HomeViewModel( } } } + + fun toggleAttendance() { + val currentState = _uiState.value + // 클럽이 선택되지 않았거나, 이미 출석/퇴실 요청이 진행 중이면 아무것도 하지 않음 + val clubId = currentState.selectedClub?.id ?: return + if (currentState.isToggleLoading) return + + viewModelScope.launch { + // 버튼 로딩 상태 시작 + _uiState.update { it.copy(isToggleLoading = true, errorMessage = null) } + + // 현재 출석 상태에 따라 checkOut 또는 checkIn 호출 + val result = if (currentState.isAttending) { + clubRepository.checkOut(clubId) + } else { + clubRepository.checkIn(clubId) + } + + when (result) { + is ApiResult.Success -> { + // 성공 시, 재실자 목록을 새로고침하여 최신 상태를 반영 + loadPresentMembers(clubId) + } + is ApiResult.Error -> { + // 실패 시, 에러 메시지 표시 + _uiState.update { it.copy(errorMessage = result.message ?: "요청에 실패했습니다.") } + } + } + + // 버튼 로딩 상태 종료 + _uiState.update { it.copy(isToggleLoading = false) } + } + } } \ No newline at end of file From 6af3b98da8e2105155e0425bc5539ab2b91c9d5a Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:14:56 +0900 Subject: [PATCH 12/14] =?UTF-8?q?[feat]:=20=EC=B6=9C=EA=B7=BC/=ED=87=B4?= =?UTF-8?q?=EA=B7=BC=ED=95=98=EA=B8=B0=20=EC=97=B0=EA=B2=B0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=9E=91=EC=84=B1=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/whosin/client/core/navigation/WhosInNavGraph.kt | 1 - .../kotlin/org/whosin/client/presentation/home/HomeScreen.kt | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/WhosInNavGraph.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/WhosInNavGraph.kt index 8a434be..9c41efe 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/WhosInNavGraph.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/WhosInNavGraph.kt @@ -127,7 +127,6 @@ fun WhosInNavGraph( composable { HomeScreen( modifier = modifier, - onNavigateBack = { navController.navigateUp() }, onNavigateToMyPage = { navController.navigate(Route.MyPage) } diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt index e750b3b..57765c4 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt @@ -238,10 +238,11 @@ fun HomeScreen( modifier = Modifier .align(Alignment.BottomCenter) .clickable( + enabled = !uiState.isToggleLoading, interactionSource = remember { MutableInteractionSource() }, - indication = null // 클릭 효과 제거 + indication = null ) { -// viewModel.toggleAttendance() // TODO: api 연결 + viewModel.toggleAttendance() }, transitionSpec = { fadeIn(animationSpec = tween(1000)) togetherWith From 021217a8f58a3f814e99a06a403c3dfa7363259b Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:29:51 +0900 Subject: [PATCH 13/14] =?UTF-8?q?[ui]:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=EC=9D=98=20=EB=8F=99=EC=95=84=EB=A6=AC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=ED=81=B4=EB=A6=AD?= =?UTF-8?q?=ED=9A=A8=EA=B3=BC=20=EB=82=98=ED=83=80=EB=82=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/home/component/MyClubSidebar.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/component/MyClubSidebar.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/component/MyClubSidebar.kt index 56cb5df..fb2af1f 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/component/MyClubSidebar.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/component/MyClubSidebar.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -73,12 +74,9 @@ private fun MyClubSidebarItem( modifier = Modifier .fillMaxWidth() .height(44.dp) - .background(color = backgroundColor, shape = RoundedCornerShape(12.dp)) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, // 클릭 효과 제거 - onClick = onClick - ) + .clip(RoundedCornerShape(12.dp)) + .background(color = backgroundColor) + .clickable(onClick = onClick) .padding(horizontal = 8.dp), contentAlignment = Alignment.CenterStart ) { From 42218bded62c890e4f1837e74ec42c81119c6483 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:30:13 +0900 Subject: [PATCH 14/14] =?UTF-8?q?[refactor]:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EC=97=B4=EB=A0=B8=EC=9D=84=20=EB=95=8C=20=EB=92=A4?= =?UTF-8?q?=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EB=88=84=EB=A5=B4=EB=A9=B4=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20=EB=8B=AB=ED=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/presentation/component/BackHandler.kt | 9 +++++++++ .../client/presentation/component/BackHandler.kt | 6 ++++++ .../org/whosin/client/presentation/home/HomeScreen.kt | 10 +++++++--- .../client/presentation/component/BackHandler.kt | 7 +++++++ .../client/presentation/component/BackHandler.jvm.kt | 7 +++++++ .../presentation/component/BackHandler.wasmJs.kt | 7 +++++++ 6 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/org/whosin/client/presentation/component/BackHandler.kt create mode 100644 composeApp/src/commonMain/kotlin/org/whosin/client/presentation/component/BackHandler.kt create mode 100644 composeApp/src/iosMain/kotlin/org/whosin/client/presentation/component/BackHandler.kt create mode 100644 composeApp/src/jvmMain/kotlin/org/whosin/client/presentation/component/BackHandler.jvm.kt create mode 100644 composeApp/src/wasmJsMain/kotlin/org/whosin/client/presentation/component/BackHandler.wasmJs.kt diff --git a/composeApp/src/androidMain/kotlin/org/whosin/client/presentation/component/BackHandler.kt b/composeApp/src/androidMain/kotlin/org/whosin/client/presentation/component/BackHandler.kt new file mode 100644 index 0000000..1df63b7 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/org/whosin/client/presentation/component/BackHandler.kt @@ -0,0 +1,9 @@ +package org.whosin.client.presentation.component + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable + +@Composable +actual fun CommonBackHandler(enabled: Boolean, onBack: () -> Unit) { + BackHandler(enabled = enabled, onBack = onBack) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/component/BackHandler.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/component/BackHandler.kt new file mode 100644 index 0000000..12d6ec5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/component/BackHandler.kt @@ -0,0 +1,6 @@ +package org.whosin.client.presentation.component + +import androidx.compose.runtime.Composable + +@Composable +expect fun CommonBackHandler(enabled: Boolean = true, onBack: () -> Unit) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt index 57765c4..74aef3e 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeScreen.kt @@ -29,12 +29,11 @@ import androidx.compose.material3.Text import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import org.whosin.client.presentation.component.CommonBackHandler import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -50,7 +49,6 @@ import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel import org.whosin.client.presentation.home.component.MyClubSidebar import org.whosin.client.presentation.home.component.PresentMembersList -import org.whosin.client.presentation.home.mock.sampleUsers import whosinclient.composeapp.generated.resources.Res import whosinclient.composeapp.generated.resources.current_whos_in_bottom import whosinclient.composeapp.generated.resources.current_whos_in_top @@ -69,6 +67,12 @@ fun HomeScreen( val scope = rememberCoroutineScope() val uiState by viewModel.uiState.collectAsStateWithLifecycle() + CommonBackHandler(enabled = drawerState.isOpen) { + scope.launch { + drawerState.close() + } + } + ModalNavigationDrawer( drawerState = drawerState, scrimColor = Color(0x66000000), diff --git a/composeApp/src/iosMain/kotlin/org/whosin/client/presentation/component/BackHandler.kt b/composeApp/src/iosMain/kotlin/org/whosin/client/presentation/component/BackHandler.kt new file mode 100644 index 0000000..34bb80e --- /dev/null +++ b/composeApp/src/iosMain/kotlin/org/whosin/client/presentation/component/BackHandler.kt @@ -0,0 +1,7 @@ +package org.whosin.client.presentation.component + +import androidx.compose.runtime.Composable + +@Composable +actual fun CommonBackHandler(enabled: Boolean, onBack: () -> Unit) { +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/org/whosin/client/presentation/component/BackHandler.jvm.kt b/composeApp/src/jvmMain/kotlin/org/whosin/client/presentation/component/BackHandler.jvm.kt new file mode 100644 index 0000000..34bb80e --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/org/whosin/client/presentation/component/BackHandler.jvm.kt @@ -0,0 +1,7 @@ +package org.whosin.client.presentation.component + +import androidx.compose.runtime.Composable + +@Composable +actual fun CommonBackHandler(enabled: Boolean, onBack: () -> Unit) { +} \ No newline at end of file diff --git a/composeApp/src/wasmJsMain/kotlin/org/whosin/client/presentation/component/BackHandler.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/org/whosin/client/presentation/component/BackHandler.wasmJs.kt new file mode 100644 index 0000000..34bb80e --- /dev/null +++ b/composeApp/src/wasmJsMain/kotlin/org/whosin/client/presentation/component/BackHandler.wasmJs.kt @@ -0,0 +1,7 @@ +package org.whosin.client.presentation.component + +import androidx.compose.runtime.Composable + +@Composable +actual fun CommonBackHandler(enabled: Boolean, onBack: () -> Unit) { +} \ No newline at end of file