diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c26475c..0d48ba7 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -52,6 +52,9 @@ android {
buildConfig = true
compose = true
}
+ androidResources {
+ noCompress += "mp4"
+ }
}
dependencies {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d47e8a1..5aa6877 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -30,16 +30,21 @@
+ android:theme="@style/Theme.JapKor.Splash">
+
diff --git a/app/src/main/java/com/apptive/japkor/LoginCallbackActivity.kt b/app/src/main/java/com/apptive/japkor/LoginCallbackActivity.kt
index c5aa01c..823d890 100644
--- a/app/src/main/java/com/apptive/japkor/LoginCallbackActivity.kt
+++ b/app/src/main/java/com/apptive/japkor/LoginCallbackActivity.kt
@@ -81,9 +81,9 @@ class LoginCallbackActivity : ComponentActivity() {
val startRoute = when (userStatus) {
UserStatus.INCOMPLETE_PROFILE -> Screen.RequiredInfo.route
+ UserStatus.CONNECTING -> Screen.Home.route
UserStatus.PENDING_APPROVAL,
UserStatus.APPROVED,
- UserStatus.CONNECTING,
UserStatus.CONNECTED,
UserStatus.BLACKLISTED -> Screen.RequiredInfoComplete.route
null -> when (needsProfileCompletion) {
diff --git a/app/src/main/java/com/apptive/japkor/SplashActivity.kt b/app/src/main/java/com/apptive/japkor/SplashActivity.kt
new file mode 100644
index 0000000..72eaca0
--- /dev/null
+++ b/app/src/main/java/com/apptive/japkor/SplashActivity.kt
@@ -0,0 +1,48 @@
+package com.apptive.japkor
+
+import android.content.Intent
+import android.media.MediaPlayer
+import android.net.Uri
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import com.apptive.japkor.widget.FullscreenVideoView
+
+class SplashActivity : ComponentActivity() {
+ private var hasNavigated = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_splash)
+
+ val videoView = findViewById(R.id.splashVideo)
+ val videoUri = Uri.parse("android.resource://${packageName}/${R.raw.n_splash}")
+ videoView.setVideoURI(videoUri)
+ videoView.setOnPreparedListener { mediaPlayer ->
+ videoView.updateVideoSize(mediaPlayer.videoWidth, mediaPlayer.videoHeight)
+ mediaPlayer.isLooping = false
+ mediaPlayer.setVideoScalingMode(
+ MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
+ )
+ videoView.start()
+ }
+ videoView.setOnCompletionListener {
+ videoView.stopPlayback()
+ navigateToMain()
+ }
+ videoView.setOnErrorListener { _, _, _ ->
+ navigateToMain()
+ true
+ }
+ }
+
+ private fun navigateToMain() {
+ if (hasNavigated) return
+ hasNavigated = true
+ val intent = Intent(this, MainActivity::class.java)
+ intent.putExtras(this.intent)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
+ startActivity(intent)
+ overridePendingTransition(0, 0)
+ finish()
+ }
+}
diff --git a/app/src/main/java/com/apptive/japkor/data/api/MatchingService.kt b/app/src/main/java/com/apptive/japkor/data/api/MatchingService.kt
new file mode 100644
index 0000000..56fc795
--- /dev/null
+++ b/app/src/main/java/com/apptive/japkor/data/api/MatchingService.kt
@@ -0,0 +1,15 @@
+package com.apptive.japkor.data.api
+
+import com.apptive.japkor.data.model.MatchingResponse
+import retrofit2.Call
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Path
+
+interface MatchingService {
+ @GET("members/matchings/female")
+ fun getFemaleMatchings(): Call>
+
+ @POST("members/matchings/{matchingId}/select")
+ fun selectMatching(@Path("matchingId") matchingId: Long): Call
+}
diff --git a/app/src/main/java/com/apptive/japkor/data/api/ServiceFactory.kt b/app/src/main/java/com/apptive/japkor/data/api/ServiceFactory.kt
index 5abddce..552d5c7 100644
--- a/app/src/main/java/com/apptive/japkor/data/api/ServiceFactory.kt
+++ b/app/src/main/java/com/apptive/japkor/data/api/ServiceFactory.kt
@@ -8,4 +8,8 @@ object ServiceFactory {
val requiredInfoApiService: RequiredInfoApiService by lazy {
ApiClient.retrofit.create(RequiredInfoApiService::class.java)
}
-}
\ No newline at end of file
+
+ val matchingService: MatchingService by lazy {
+ ApiClient.retrofit.create(MatchingService::class.java)
+ }
+}
diff --git a/app/src/main/java/com/apptive/japkor/data/local/DataStoreManager.kt b/app/src/main/java/com/apptive/japkor/data/local/DataStoreManager.kt
index e5f756a..6241f35 100644
--- a/app/src/main/java/com/apptive/japkor/data/local/DataStoreManager.kt
+++ b/app/src/main/java/com/apptive/japkor/data/local/DataStoreManager.kt
@@ -87,6 +87,15 @@ class DataStoreManager(private val context: Context) {
}
}
+ suspend fun clearUserInfo() {
+ context.dataStore.edit { prefs ->
+ prefs.remove(KEY_MEMBER_ID)
+ prefs.remove(KEY_NAME)
+ prefs.remove(KEY_TOKEN)
+ prefs.remove(KEY_STATUS)
+ }
+ }
+
suspend fun clear() {
context.dataStore.edit { it.clear() }
}
diff --git a/app/src/main/java/com/apptive/japkor/data/model/Matching.kt b/app/src/main/java/com/apptive/japkor/data/model/Matching.kt
new file mode 100644
index 0000000..5153742
--- /dev/null
+++ b/app/src/main/java/com/apptive/japkor/data/model/Matching.kt
@@ -0,0 +1,13 @@
+package com.apptive.japkor.data.model
+
+data class MatchingResponse(
+ val matchingId: Long,
+ val maleMemberId: Long,
+ val maleName: String,
+ val maleEmail: String,
+ val height: Int?,
+ val weight: Int?,
+ val residenceArea: String?,
+ val matchingOrder: Int,
+ val status: String
+)
diff --git a/app/src/main/java/com/apptive/japkor/navigation/AppNavHost.kt b/app/src/main/java/com/apptive/japkor/navigation/AppNavHost.kt
index c2136ce..635d0b1 100644
--- a/app/src/main/java/com/apptive/japkor/navigation/AppNavHost.kt
+++ b/app/src/main/java/com/apptive/japkor/navigation/AppNavHost.kt
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
+import com.apptive.japkor.ui.main.MainRouteScreen
import com.apptive.japkor.ui.language.LanguageScreen
import com.apptive.japkor.ui.login.LoginScreen
import com.apptive.japkor.ui.requiredinfo.RequiredInfoCompleteScreen
@@ -19,6 +20,8 @@ sealed class Screen(val route: String) {
object Language : Screen("language")
object SignUp : Screen("signup")
+ object Home : Screen("home")
+
object RequiredInfo : Screen("requiredinfo")
object RequiredInfoComplete : Screen("requiredinfo_complete")
diff --git a/app/src/main/java/com/apptive/japkor/ui/localization/Localization.kt b/app/src/main/java/com/apptive/japkor/ui/localization/Localization.kt
index 125f318..b804ef2 100644
--- a/app/src/main/java/com/apptive/japkor/ui/localization/Localization.kt
+++ b/app/src/main/java/com/apptive/japkor/ui/localization/Localization.kt
@@ -33,6 +33,9 @@ object AppLocalizer {
"로그인 성공! 환영합니다." to "ログイン成功!ようこそ。",
"로그인 실패! 이메일과 비밀번호를 확인해주세요." to "ログイン失敗。メールアドレスとパスワードを確認してください。",
"로그인" to "ログイン",
+ "로그아웃" to "ログアウト",
+ "정말 로그아웃 하시겠어요?" to "本当にログアウトしますか?",
+ "취소" to "キャンセル",
"아이디 찾기" to "IDを探す",
"비밀번호 찾기" to "パスワードを探す",
"회원가입" to "会員登録",
diff --git a/app/src/main/java/com/apptive/japkor/ui/main/MainRouteScreen.kt b/app/src/main/java/com/apptive/japkor/ui/main/MainRouteScreen.kt
new file mode 100644
index 0000000..a8c60d9
--- /dev/null
+++ b/app/src/main/java/com/apptive/japkor/ui/main/MainRouteScreen.kt
@@ -0,0 +1,301 @@
+package com.apptive.japkor.ui.main
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationBarItemDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+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.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import com.apptive.japkor.R
+import com.apptive.japkor.data.local.DataStoreManager
+import com.apptive.japkor.data.local.TokenProvider
+import com.apptive.japkor.navigation.Screen
+import com.apptive.japkor.ui.components.CustomText
+import com.apptive.japkor.ui.components.CustomTextType
+import com.apptive.japkor.ui.components.LoadingDialog
+import com.apptive.japkor.ui.components.LocalToastManager
+import com.apptive.japkor.ui.components.ToastType
+import com.apptive.japkor.ui.main.chat.ChattingScreen
+import com.apptive.japkor.ui.main.home.HomeScreen
+import com.apptive.japkor.ui.main.home.HomeUiEvent
+import com.apptive.japkor.ui.main.home.HomeViewModel
+import com.apptive.japkor.ui.main.mypage.MypageScreen
+import com.apptive.japkor.ui.main.setting.SettingScreen
+import com.apptive.japkor.ui.theme.CustomColor
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+private data class HomeTab(
+ val route: String,
+ val label: String,
+ val iconResId: Int,
+ val selectedIconResId: Int
+)
+
+private object HomeRoute {
+ const val Chat = "home_chat"
+ const val Main = "home_main"
+ const val MyPage = "home_mypage"
+ const val Setting = "home_setting"
+}
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
+fun MainRouteScreen(
+ navController: NavController,
+ viewModel: HomeViewModel = viewModel()
+) {
+ val homeNavController = rememberNavController()
+ val navBackStackEntry by homeNavController.currentBackStackEntryAsState()
+ val currentRoute = navBackStackEntry?.destination?.route ?: HomeRoute.Main
+ val isMainScreen = currentRoute == HomeRoute.Main
+ val isSettingScreen = currentRoute == HomeRoute.Setting
+ val showBottomBar = currentRoute in setOf(HomeRoute.Chat, HomeRoute.Main, HomeRoute.MyPage)
+ val tabs = listOf(
+ HomeTab(
+ route = HomeRoute.Chat,
+ label = "채팅",
+ iconResId = R.drawable.ic_chat,
+ selectedIconResId = R.drawable.ic_chat_filled
+ ),
+ HomeTab(
+ route = HomeRoute.Main,
+ label = "홈",
+ iconResId = R.drawable.ic_n,
+ selectedIconResId = R.drawable.ic_n
+ ),
+ HomeTab(
+ route = HomeRoute.MyPage,
+ label = "내정보",
+ iconResId = R.drawable.ic_user,
+ selectedIconResId = R.drawable.ic_user_filled
+ )
+ )
+
+ var showLogoutDialog by remember { mutableStateOf(false) }
+ val toastManager = LocalToastManager.current
+ val uiState by viewModel.uiState.collectAsState()
+ val matchings = uiState.matchings
+ val selectedMatching = uiState.selectedMatching
+ val pagerState = rememberPagerState(pageCount = { matchings.size })
+ val context = LocalContext.current
+ val dataStoreManager = remember { DataStoreManager(context) }
+ val coroutineScope = rememberCoroutineScope()
+
+ BackHandler(enabled = isMainScreen && selectedMatching != null) {
+ viewModel.hideDetails()
+ }
+
+ LaunchedEffect(Unit) {
+ viewModel.events.collectLatest { event ->
+ when (event) {
+ is HomeUiEvent.ShowToast -> {
+ when (event.type) {
+ ToastType.INFO -> toastManager.info(event.message)
+ ToastType.SUCCESS -> toastManager.success(event.message)
+ ToastType.ERROR -> toastManager.error(event.message)
+ }
+ }
+ }
+ }
+ }
+
+ LaunchedEffect(matchings.size) {
+ if (matchings.isNotEmpty() && pagerState.currentPage >= matchings.size) {
+ pagerState.scrollToPage(0)
+ }
+ }
+
+ if (uiState.isLoading) {
+ LoadingDialog()
+ }
+
+ Scaffold(
+ containerColor = CustomColor.white,
+ topBar = {
+ TopAppBar(
+ title = {},
+ navigationIcon = {
+ when {
+ isSettingScreen -> {
+ IconButton(onClick = { homeNavController.popBackStack() }) {
+ Icon(
+ painter = painterResource(R.drawable.ic_back),
+ contentDescription = "뒤로가기"
+ )
+ }
+ }
+ isMainScreen && selectedMatching != null -> {
+ IconButton(onClick = { viewModel.hideDetails() }) {
+ Icon(
+ painter = painterResource(R.drawable.ic_back),
+ contentDescription = "뒤로가기"
+ )
+ }
+ }
+ }
+ },
+ actions = {
+ if (!isSettingScreen) {
+ IconButton(onClick = { homeNavController.navigate(HomeRoute.Setting) }) {
+ Icon(
+ painter = painterResource(R.drawable.ic_settings),
+ contentDescription = "설정"
+ )
+ }
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = CustomColor.white
+ )
+ )
+ },
+ bottomBar = {
+ if (showBottomBar) {
+ NavigationBar(
+ containerColor = CustomColor.white
+ ) {
+ tabs.forEach { tab ->
+ val isSelected = currentRoute == tab.route
+ NavigationBarItem(
+ selected = isSelected,
+ onClick = {
+ homeNavController.navigate(tab.route) {
+ popUpTo(homeNavController.graph.findStartDestination().id) {
+ saveState = true
+ }
+ launchSingleTop = true
+ restoreState = true
+ }
+ },
+ icon = {
+ Icon(
+ painter = painterResource(
+ if (isSelected) tab.selectedIconResId else tab.iconResId
+ ),
+ contentDescription = tab.label
+ )
+ },
+ colors = NavigationBarItemDefaults.colors(
+ selectedIconColor = CustomColor.primary600,
+ selectedTextColor = CustomColor.primary600,
+ unselectedIconColor = CustomColor.gray400,
+ unselectedTextColor = CustomColor.gray400,
+ indicatorColor = CustomColor.white
+ )
+ )
+ }
+ }
+ }
+ }
+ ) { innerPadding ->
+ NavHost(
+ navController = homeNavController,
+ startDestination = HomeRoute.Main,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ ) {
+ composable(HomeRoute.Chat) {
+ ChattingScreen()
+ }
+ composable(HomeRoute.Main) {
+ HomeScreen(
+ uiState = uiState,
+ pagerState = pagerState,
+ onShowDetails = { viewModel.showDetails(it) },
+ onNoMatch = { viewModel.noMatchSelected() },
+ onConfirm = { viewModel.selectMatching(it) },
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ composable(HomeRoute.MyPage) {
+ MypageScreen()
+ }
+ composable(HomeRoute.Setting) {
+ SettingScreen(
+ onLogoutClick = { showLogoutDialog = true },
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+ }
+
+ if (showLogoutDialog) {
+ AlertDialog(
+ onDismissRequest = { showLogoutDialog = false },
+ title = {
+ CustomText(
+ text = "로그아웃",
+ type = CustomTextType.title,
+ color = CustomColor.black
+ )
+ },
+ text = {
+ CustomText(
+ text = "정말 로그아웃 하시겠어요?",
+ type = CustomTextType.body,
+ color = CustomColor.gray400
+ )
+ },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ showLogoutDialog = false
+ TokenProvider.clearToken()
+ coroutineScope.launch {
+ dataStoreManager.clearUserInfo()
+ }
+ navController.navigate(Screen.Login.route) {
+ popUpTo(navController.graph.startDestinationId) { inclusive = true }
+ launchSingleTop = true
+ }
+ }
+ ) {
+ CustomText(
+ text = "로그아웃",
+ type = CustomTextType.body,
+ color = CustomColor.primary600
+ )
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showLogoutDialog = false }) {
+ CustomText(
+ text = "취소",
+ type = CustomTextType.body,
+ color = CustomColor.gray400
+ )
+ }
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/com/apptive/japkor/ui/main/chat/ChattingScreen.kt b/app/src/main/java/com/apptive/japkor/ui/main/chat/ChattingScreen.kt
new file mode 100644
index 0000000..4470072
--- /dev/null
+++ b/app/src/main/java/com/apptive/japkor/ui/main/chat/ChattingScreen.kt
@@ -0,0 +1,24 @@
+package com.apptive.japkor.ui.main.chat
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.apptive.japkor.ui.components.CustomText
+import com.apptive.japkor.ui.components.CustomTextType
+import com.apptive.japkor.ui.theme.CustomColor
+
+@Composable
+fun ChattingScreen(modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ CustomText(
+ text = "매칭을 기다려주세요..",
+ type = CustomTextType.body,
+ color = CustomColor.gray400
+ )
+ }
+}
diff --git a/app/src/main/java/com/apptive/japkor/ui/main/home/HomeScreen.kt b/app/src/main/java/com/apptive/japkor/ui/main/home/HomeScreen.kt
new file mode 100644
index 0000000..93c6096
--- /dev/null
+++ b/app/src/main/java/com/apptive/japkor/ui/main/home/HomeScreen.kt
@@ -0,0 +1,387 @@
+package com.apptive.japkor.ui.main.home
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+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.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
+import com.apptive.japkor.R
+import com.apptive.japkor.data.model.MatchingResponse
+import com.apptive.japkor.ui.components.CustomText
+import com.apptive.japkor.ui.components.CustomTextType
+import com.apptive.japkor.ui.theme.CustomColor
+import kotlin.math.absoluteValue
+
+@Composable
+fun HomeScreen(
+ uiState: HomeUiState,
+ pagerState: PagerState,
+ onShowDetails: (MatchingResponse) -> Unit,
+ onNoMatch: () -> Unit,
+ onConfirm: (Long) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val matchings = uiState.matchings
+ val selectedMatching = uiState.selectedMatching
+
+ Box(
+ modifier = modifier
+ ) {
+ when {
+ selectedMatching != null -> {
+ MatchingDetailContent(
+ matching = selectedMatching,
+ onConfirm = { onConfirm(selectedMatching.matchingId) }
+ )
+ }
+ uiState.isWaiting || matchings.isEmpty() -> {
+ WaitingContent(modifier = Modifier.fillMaxSize())
+ }
+ else -> {
+ MatchingCarouselContent(
+ matchings = matchings,
+ pagerState = pagerState,
+ onShowDetails = onShowDetails,
+ onNoMatch = onNoMatch
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun WaitingContent(modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier,
+ contentAlignment = Alignment.Center
+ ) {
+ CustomText(
+ text = "매칭 진행 중입니다..",
+ type = CustomTextType.body,
+ color = CustomColor.gray400
+ )
+ }
+}
+
+@Composable
+@OptIn(ExperimentalFoundationApi::class)
+private fun MatchingCarouselContent(
+ matchings: List,
+ pagerState: PagerState,
+ onShowDetails: (MatchingResponse) -> Unit,
+ onNoMatch: () -> Unit
+) {
+ val currentMatching = matchings.getOrNull(pagerState.currentPage)
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 24.dp, vertical = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ CustomText(
+ text = "매칭된 남성",
+ type = CustomTextType.title,
+ color = CustomColor.black
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+ HorizontalPager(
+ state = pagerState,
+ contentPadding = PaddingValues(horizontal = 32.dp),
+ pageSpacing = 16.dp,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(360.dp)
+ ) { page ->
+ val matching = matchings[page]
+ val pageOffset = (
+ (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction
+ ).absoluteValue
+ val scale = lerp(0.92f, 1f, 1f - pageOffset.coerceIn(0f, 1f))
+
+ MatchingCard(
+ matching = matching,
+ modifier = Modifier
+ .fillMaxSize()
+ .graphicsLayer {
+ scaleX = scale
+ scaleY = scale
+ }
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ PagerIndicator(
+ total = matchings.size,
+ current = pagerState.currentPage
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ Button(
+ onClick = { currentMatching?.let { onShowDetails(it) } },
+ enabled = currentMatching != null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = CustomColor.primary600,
+ disabledContainerColor = CustomColor.primary300
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ CustomText(
+ text = "이 분 프로필이 궁금해요",
+ type = CustomTextType.body,
+ color = Color.White
+ )
+ }
+ Spacer(modifier = Modifier.height(12.dp))
+ Button(
+ onClick = onNoMatch,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = CustomColor.gray100
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ CustomText(
+ text = "마음에 드는 상대가 없어요",
+ type = CustomTextType.body,
+ color = CustomColor.black
+ )
+ }
+ }
+}
+
+@Composable
+private fun MatchingCard(
+ matching: MatchingResponse,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier,
+ shape = RoundedCornerShape(24.dp),
+ colors = CardDefaults.cardColors(containerColor = CustomColor.gray100)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .size(160.dp)
+ .background(CustomColor.primary100, CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_user),
+ contentDescription = "프로필 이미지",
+ tint = CustomColor.primary600,
+ modifier = Modifier.size(72.dp)
+ )
+ }
+ Spacer(modifier = Modifier.height(24.dp))
+ CustomText(
+ text = matching.maleName,
+ type = CustomTextType.headline,
+ color = CustomColor.black,
+ textAlign = TextAlign.Center
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ CustomText(
+ text = "프로필을 확인해보세요",
+ type = CustomTextType.body,
+ color = CustomColor.gray400
+ )
+ }
+ }
+}
+
+@Composable
+private fun PagerIndicator(total: Int, current: Int) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ repeat(total) { index ->
+ val color = if (index == current) CustomColor.primary600 else CustomColor.gray200
+ Box(
+ modifier = Modifier
+ .size(8.dp)
+ .background(color, CircleShape)
+ )
+ if (index != total - 1) {
+ Spacer(modifier = Modifier.width(6.dp))
+ }
+ }
+ }
+}
+
+@Composable
+private fun MatchingDetailContent(
+ matching: MatchingResponse,
+ onConfirm: () -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 24.dp, vertical = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ LazyColumn(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ item {
+ ProfileHeader(matching = matching)
+ }
+ item {
+ DetailCard(matching = matching)
+ }
+ }
+ Button(
+ onClick = onConfirm,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = CustomColor.primary600
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ CustomText(
+ text = "마음에 들어요 매칭해주세요",
+ type = CustomTextType.body,
+ color = Color.White
+ )
+ }
+ }
+}
+
+@Composable
+private fun ProfileHeader(matching: MatchingResponse) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(72.dp)
+ .background(CustomColor.primary100, CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_user),
+ contentDescription = "프로필 이미지",
+ tint = CustomColor.primary600,
+ modifier = Modifier.size(36.dp)
+ )
+ }
+ Spacer(modifier = Modifier.width(16.dp))
+ Column {
+ CustomText(
+ text = matching.maleName,
+ type = CustomTextType.headline,
+ color = CustomColor.black
+ )
+ CustomText(
+ text = matching.maleEmail,
+ type = CustomTextType.body,
+ color = CustomColor.gray400
+ )
+ }
+ }
+}
+
+@Composable
+private fun DetailCard(matching: MatchingResponse) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(20.dp),
+ colors = CardDefaults.cardColors(containerColor = CustomColor.gray100)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ CustomText(
+ text = "상세 정보",
+ type = CustomTextType.title,
+ color = CustomColor.black
+ )
+ DetailItem(label = "매칭 ID", value = matching.matchingId.toString())
+ DetailItem(label = "남성 회원 ID", value = matching.maleMemberId.toString())
+ DetailItem(label = "이름", value = matching.maleName)
+ DetailItem(label = "이메일", value = matching.maleEmail)
+ DetailItem(label = "키", value = formatHeight(matching.height))
+ DetailItem(label = "몸무게", value = formatWeight(matching.weight))
+ DetailItem(label = "거주지역", value = formatText(matching.residenceArea))
+ DetailItem(label = "매칭 순서", value = matching.matchingOrder.toString())
+ DetailItem(label = "상태", value = matching.status)
+ }
+ }
+}
+
+@Composable
+private fun DetailItem(label: String, value: String) {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ CustomText(
+ text = label,
+ type = CustomTextType.label,
+ color = CustomColor.gray400
+ )
+ CustomText(
+ text = value,
+ type = CustomTextType.body,
+ color = CustomColor.black
+ )
+ }
+}
+
+private fun formatHeight(value: Int?): String {
+ return value?.let { "${it}cm" } ?: "미입력"
+}
+
+private fun formatWeight(value: Int?): String {
+ return value?.let { "${it}kg" } ?: "미입력"
+}
+
+private fun formatText(value: String?): String {
+ return value?.takeIf { it.isNotBlank() } ?: "미입력"
+}
diff --git a/app/src/main/java/com/apptive/japkor/ui/main/home/HomeViewModel.kt b/app/src/main/java/com/apptive/japkor/ui/main/home/HomeViewModel.kt
new file mode 100644
index 0000000..455f595
--- /dev/null
+++ b/app/src/main/java/com/apptive/japkor/ui/main/home/HomeViewModel.kt
@@ -0,0 +1,109 @@
+package com.apptive.japkor.ui.main.home
+
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.apptive.japkor.data.api.MatchingService
+import com.apptive.japkor.data.api.ServiceFactory
+import com.apptive.japkor.data.model.MatchingResponse
+import com.apptive.japkor.ui.components.ToastType
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import retrofit2.awaitResponse
+
+sealed class HomeUiEvent {
+ data class ShowToast(val message: String, val type: ToastType = ToastType.ERROR) : HomeUiEvent()
+}
+
+data class HomeUiState(
+ val isLoading: Boolean = false,
+ val matchings: List = emptyList(),
+ val selectedMatching: MatchingResponse? = null,
+ val isWaiting: Boolean = false
+)
+
+class HomeViewModel(
+ private val matchingService: MatchingService = ServiceFactory.matchingService
+) : ViewModel() {
+
+ private val _uiState = MutableStateFlow(HomeUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ private val _events = MutableSharedFlow(extraBufferCapacity = 1)
+ val events: SharedFlow = _events.asSharedFlow()
+
+ init {
+ fetchFemaleMatchings()
+ }
+
+ fun fetchFemaleMatchings() {
+ if (_uiState.value.isWaiting) return
+
+ viewModelScope.launch {
+ _uiState.update { it.copy(isLoading = true) }
+ runCatching {
+ matchingService.getFemaleMatchings().awaitResponse()
+ }.onSuccess { response ->
+ Log.d(TAG, "getFemaleMatchings success=${response.isSuccessful} code=${response.code()}")
+ if (response.isSuccessful) {
+ val data = response.body().orEmpty()
+ if (data.isEmpty()) {
+ _uiState.value = HomeUiState(isWaiting = true)
+ } else {
+ _uiState.value = HomeUiState(matchings = data)
+ }
+ } else {
+ _uiState.value = HomeUiState(isWaiting = true)
+ _events.tryEmit(HomeUiEvent.ShowToast("매칭 목록을 불러오지 못했습니다."))
+ }
+ }.onFailure { throwable ->
+ Log.e(TAG, "getFemaleMatchings failed", throwable)
+ _uiState.value = HomeUiState(isWaiting = true)
+ _events.tryEmit(HomeUiEvent.ShowToast("네트워크 오류로 매칭을 불러올 수 없습니다."))
+ }
+ }
+ }
+
+ fun showDetails(matching: MatchingResponse) {
+ _uiState.update { it.copy(selectedMatching = matching) }
+ }
+
+ fun hideDetails() {
+ _uiState.update { it.copy(selectedMatching = null) }
+ }
+
+ fun selectMatching(matchingId: Long) {
+ viewModelScope.launch {
+ _uiState.update { it.copy(isLoading = true) }
+ runCatching {
+ matchingService.selectMatching(matchingId).awaitResponse()
+ }.onSuccess { response ->
+ Log.d(TAG, "selectMatching success=${response.isSuccessful} code=${response.code()}")
+ if (response.isSuccessful) {
+ _uiState.value = HomeUiState(isWaiting = true)
+ } else {
+ _uiState.update { it.copy(isLoading = false) }
+ _events.tryEmit(HomeUiEvent.ShowToast("매칭 선택에 실패했습니다."))
+ }
+ }.onFailure { throwable ->
+ Log.e(TAG, "selectMatching failed", throwable)
+ _uiState.update { it.copy(isLoading = false) }
+ _events.tryEmit(HomeUiEvent.ShowToast("네트워크 오류로 매칭을 선택할 수 없습니다."))
+ }
+ }
+ }
+
+ fun noMatchSelected() {
+ _uiState.value = HomeUiState(isWaiting = true)
+ }
+
+ companion object {
+ private const val TAG = "HomeViewModel"
+ }
+}
diff --git a/app/src/main/java/com/apptive/japkor/ui/main/mypage/MypageScreen.kt b/app/src/main/java/com/apptive/japkor/ui/main/mypage/MypageScreen.kt
new file mode 100644
index 0000000..4d4c23d
--- /dev/null
+++ b/app/src/main/java/com/apptive/japkor/ui/main/mypage/MypageScreen.kt
@@ -0,0 +1,24 @@
+package com.apptive.japkor.ui.main.mypage
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.apptive.japkor.ui.components.CustomText
+import com.apptive.japkor.ui.components.CustomTextType
+import com.apptive.japkor.ui.theme.CustomColor
+
+@Composable
+fun MypageScreen(modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ CustomText(
+ text = "내정보 페이지입니다.",
+ type = CustomTextType.body,
+ color = CustomColor.gray400
+ )
+ }
+}
diff --git a/app/src/main/java/com/apptive/japkor/ui/main/setting/SettingScreen.kt b/app/src/main/java/com/apptive/japkor/ui/main/setting/SettingScreen.kt
new file mode 100644
index 0000000..021aace
--- /dev/null
+++ b/app/src/main/java/com/apptive/japkor/ui/main/setting/SettingScreen.kt
@@ -0,0 +1,46 @@
+package com.apptive.japkor.ui.main.setting
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+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.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.apptive.japkor.ui.components.CustomText
+import com.apptive.japkor.ui.components.CustomTextType
+import com.apptive.japkor.ui.theme.CustomColor
+
+@Composable
+fun SettingScreen(
+ onLogoutClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(horizontal = 24.dp, vertical = 16.dp),
+ verticalArrangement = Arrangement.Top
+ ) {
+ Button(
+ onClick = onLogoutClick,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = CustomColor.primary600
+ )
+ ) {
+ CustomText(
+ text = "로그아웃",
+ type = CustomTextType.body,
+ color = Color.White
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/apptive/japkor/utils/required_info/RequiredInfoMapper.kt b/app/src/main/java/com/apptive/japkor/utils/required_info/RequiredInfoMapper.kt
index 47a8671..c325ad7 100644
--- a/app/src/main/java/com/apptive/japkor/utils/required_info/RequiredInfoMapper.kt
+++ b/app/src/main/java/com/apptive/japkor/utils/required_info/RequiredInfoMapper.kt
@@ -42,7 +42,7 @@ object RequiredInfoMapper {
"불교" -> "BUDDHISM"
"기독교" -> "CHRISTIANITY"
"천주교" -> "CATHOLICISM"
- "신토" -> "SHINTO"
+ "신토" -> "TOISM"
"기타" -> "OTHER"
else -> null
}
diff --git a/app/src/main/java/com/apptive/japkor/widget/FullscreenVideoView.kt b/app/src/main/java/com/apptive/japkor/widget/FullscreenVideoView.kt
new file mode 100644
index 0000000..d08f861
--- /dev/null
+++ b/app/src/main/java/com/apptive/japkor/widget/FullscreenVideoView.kt
@@ -0,0 +1,43 @@
+package com.apptive.japkor.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.VideoView
+
+class FullscreenVideoView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : VideoView(context, attrs, defStyleAttr) {
+ private var videoWidth = 0
+ private var videoHeight = 0
+
+ fun updateVideoSize(width: Int, height: Int) {
+ if (width <= 0 || height <= 0) return
+ videoWidth = width
+ videoHeight = height
+ requestLayout()
+ invalidate()
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val viewWidth = MeasureSpec.getSize(widthMeasureSpec)
+ val viewHeight = MeasureSpec.getSize(heightMeasureSpec)
+
+ if (videoWidth == 0 || videoHeight == 0 || viewWidth == 0 || viewHeight == 0) {
+ setMeasuredDimension(viewWidth, viewHeight)
+ return
+ }
+
+ val viewRatio = viewWidth.toFloat() / viewHeight
+ val videoRatio = videoWidth.toFloat() / videoHeight
+
+ if (videoRatio > viewRatio) {
+ val scaledWidth = (viewHeight * videoRatio).toInt()
+ setMeasuredDimension(scaledWidth, viewHeight)
+ } else {
+ val scaledHeight = (viewWidth / videoRatio).toInt()
+ setMeasuredDimension(viewWidth, scaledHeight)
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/ic_chat.xml b/app/src/main/res/drawable/ic_chat.xml
new file mode 100644
index 0000000..9141302
--- /dev/null
+++ b/app/src/main/res/drawable/ic_chat.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_chat_filled.xml b/app/src/main/res/drawable/ic_chat_filled.xml
new file mode 100644
index 0000000..dcfcaaf
--- /dev/null
+++ b/app/src/main/res/drawable/ic_chat_filled.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_n.xml b/app/src/main/res/drawable/ic_n.xml
new file mode 100644
index 0000000..9990e1c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_n.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml
new file mode 100644
index 0000000..b0e49e1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_settings.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_user.xml b/app/src/main/res/drawable/ic_user.xml
new file mode 100644
index 0000000..afa155f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_user.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_user_filled.xml b/app/src/main/res/drawable/ic_user_filled.xml
new file mode 100644
index 0000000..dbeeba1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_user_filled.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml
new file mode 100644
index 0000000..8ea8fbb
--- /dev/null
+++ b/app/src/main/res/layout/activity_splash.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/app/src/main/res/raw/n_splash.mp4 b/app/src/main/res/raw/n_splash.mp4
new file mode 100644
index 0000000..64d7076
Binary files /dev/null and b/app/src/main/res/raw/n_splash.mp4 differ
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 8882875..7ebb500 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -2,4 +2,13 @@
-
\ No newline at end of file
+
+
+