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"> 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/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/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 } } 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 c7f75ca..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 @@ -1,9 +1,96 @@ 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 +import org.whosin.client.data.dto.response.ClubPresencesResponseDto +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) + } + } + + 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) + } + } + + 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 79a7830..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 @@ -1,9 +1,22 @@ 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 class ClubRepository( private val dataSource: RemoteClubDataSource ) { + suspend fun getMyClubs(): ApiResult = + dataSource.getMyClubs() + 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 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 7f3142e..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 @@ -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 @@ -28,26 +29,26 @@ 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 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 import whosinclient.composeapp.generated.resources.Res import whosinclient.composeapp.generated.resources.current_whos_in_bottom import whosinclient.composeapp.generated.resources.current_whos_in_top @@ -59,16 +60,18 @@ import whosinclient.composeapp.generated.resources.people_count @Composable 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()) } + CommonBackHandler(enabled = drawerState.isOpen) { + scope.launch { + drawerState.close() + } + } ModalNavigationDrawer( drawerState = drawerState, @@ -81,10 +84,14 @@ 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) + } + scope.launch { drawerState.close() } }, onClose = { scope.launch { drawerState.close() } @@ -140,7 +147,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, @@ -168,7 +175,7 @@ fun HomeScreen( modifier = Modifier .padding(start = 4.dp, bottom = 3.dp) .size(24.dp) -// .clickable(onClick = onNavigateToMyPage) + .clickable { viewModel.refresh() } ) } } @@ -178,13 +185,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), @@ -213,19 +224,29 @@ 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( + enabled = !uiState.isToggleLoading, interactionSource = remember { MutableInteractionSource() }, - indication = null // 클릭 효과 제거 + indication = null ) { - isAttending = !isAttending + viewModel.toggleAttendance() }, transitionSpec = { fadeIn(animationSpec = tween(1000)) togetherWith @@ -254,7 +275,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/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/home/HomeViewModel.kt index cdf5121..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 @@ -1,10 +1,141 @@ 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 PresentMemberUi( + val userName: String, + val isMe: Boolean +) + +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, + val presentMembers: List = emptyList(), + 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, 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 ?: "재실자 명단을 불러오지 못했습니다." + ) + } + } + } + } + } + + 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 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 ) { 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), ) 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 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 + + + +