From 4ca7a0b4155afec01e973ea44a572cd4cb932d5c Mon Sep 17 00:00:00 2001 From: lhs2257 Date: Thu, 24 Jul 2025 15:01:02 +0900 Subject: [PATCH 001/224] =?UTF-8?q?[Test]=20=EC=BB=A4=EB=B0=8B=20&=20?= =?UTF-8?q?=EB=A8=B8=EC=A7=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/example/chaining/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/example/chaining/MainActivity.kt b/app/src/main/java/com/example/chaining/MainActivity.kt index af01886..15772cf 100644 --- a/app/src/main/java/com/example/chaining/MainActivity.kt +++ b/app/src/main/java/com/example/chaining/MainActivity.kt @@ -36,7 +36,7 @@ fun greeting( modifier: Modifier = Modifier, ) { Text( - text = "Hello $name!", + text = "Hi $name!", modifier = modifier, ) } From 46be690c80e59dbf0668342ec5c73b7f8531e6b4 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 4 Aug 2025 14:28:11 +0900 Subject: [PATCH 002/224] =?UTF-8?q?[Feat]=20=ED=8C=8C=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EA=B5=AC=EA=B8=80=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 19 +++- app/google-services.json | 22 ++++- app/src/main/AndroidManifest.xml | 3 + .../java/com/example/chaining/MainActivity.kt | 55 ++++------- .../example/chaining/ui/login/LoginScreen.kt | 95 +++++++++++++++++++ .../example/chaining/ui/screen/HomeScreen.kt | 34 +++++++ 6 files changed, 189 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt create mode 100644 app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c6c2c04..894ddc7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) @@ -11,9 +13,13 @@ plugins { id("org.jetbrains.kotlin.plugin.compose") } +val properties = Properties().apply { + load(rootProject.file("local.properties").inputStream()) +} + android { namespace = "com.example.chaining" - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "com.example.chaining" @@ -22,6 +28,8 @@ android { versionCode = 1 versionName = "1.0" + buildConfigField("String","GOOGLE_API_WEB_CLIENT_ID", properties["GOOGLE_API_WEB_CLIENT_ID"].toString()) + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true @@ -46,6 +54,7 @@ android { } buildFeatures { compose = true + buildConfig = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" @@ -58,6 +67,14 @@ android { } dependencies { + // Google Sign-In (Credentials API 포함) + implementation("androidx.credentials:credentials:1.5.0") + implementation("androidx.credentials:credentials-play-services-auth:1.5.0") + + // Google Identity Services (Google 로그인 팝업 등을 위해 필요) + implementation("com.google.android.gms:play-services-auth:21.0.0") + implementation("com.google.android.libraries.identity.googleid:googleid:1.1.0") + // 구글 Firebase 사용 implementation(platform("com.google.firebase:firebase-bom:33.16.0")) implementation("com.google.firebase:firebase-analytics") diff --git a/app/google-services.json b/app/google-services.json index 7626976..2cb4281 100644 --- a/app/google-services.json +++ b/app/google-services.json @@ -12,7 +12,20 @@ "package_name": "com.example.chaining" } }, - "oauth_client": [], + "oauth_client": [ + { + "client_id": "719736077401-t6tu672tn4m7mt7arskef2gk2oj8u3cn.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.example.chaining", + "certificate_hash": "572654d77cdf963728ceebe9a15c3ea4e1a972a7" + } + }, + { + "client_id": "719736077401-smtbf85pqoghs04i3vam5rflrjfoaovu.apps.googleusercontent.com", + "client_type": 3 + } + ], "api_key": [ { "current_key": "AIzaSyDIcZyjKx3aaxdM75g8P7PvZgU4WJSco8s" @@ -20,7 +33,12 @@ ], "services": { "appinvite_service": { - "other_platform_oauth_client": [] + "other_platform_oauth_client": [ + { + "client_id": "719736077401-smtbf85pqoghs04i3vam5rflrjfoaovu.apps.googleusercontent.com", + "client_type": 3 + } + ] } } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7405ee9..80bf54a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,6 +24,9 @@ + \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/MainActivity.kt b/app/src/main/java/com/example/chaining/MainActivity.kt index 15772cf..70dd80c 100644 --- a/app/src/main/java/com/example/chaining/MainActivity.kt +++ b/app/src/main/java/com/example/chaining/MainActivity.kt @@ -3,48 +3,31 @@ package com.example.chaining import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.example.chaining.ui.theme.chainingTheme +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.example.chaining.ui.screen.HomeScreen +import com.example.chaining.ui.login.LoginScreen +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.auth.auth class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - enableEdgeToEdge() + FirebaseApp.initializeApp(this) + setContent { - chainingTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - greeting( - name = "Android", - modifier = Modifier.padding(innerPadding), - ) + var isLoggedIn by remember { mutableStateOf(Firebase.auth.currentUser != null) } + + if (isLoggedIn) { + HomeScreen() + } else { + LoginScreen { + isLoggedIn = true } } } } -} - -@Composable -fun greeting( - name: String, - modifier: Modifier = Modifier, -) { - Text( - text = "Hi $name!", - modifier = modifier, - ) -} - -@Preview(showBackground = true) -@Composable -fun greetingPreview() { - chainingTheme { - greeting("Android") - } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt b/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt new file mode 100644 index 0000000..765e7be --- /dev/null +++ b/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt @@ -0,0 +1,95 @@ +package com.example.chaining.ui.login + +import android.app.Activity +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.example.chaining.BuildConfig +import com.google.android.gms.auth.api.identity.BeginSignInRequest +import com.google.android.gms.auth.api.identity.Identity +import com.google.firebase.Firebase +import com.google.firebase.auth.GoogleAuthProvider +import com.google.firebase.auth.auth + +@Composable +fun LoginScreen(onLoginSuccess: () -> Unit) { + val context = LocalContext.current + val signInClient = Identity.getSignInClient(context) + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartIntentSenderForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val intent = result.data + if (intent != null) { + val credential = signInClient.getSignInCredentialFromIntent(intent) + val idToken = credential.googleIdToken + + if (idToken != null) { + val firebaseCredential = GoogleAuthProvider.getCredential(idToken, null) + Firebase.auth.signInWithCredential(firebaseCredential) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + onLoginSuccess() + } else { + Toast.makeText(context, "로그인 실패", Toast.LENGTH_SHORT).show() + } + } + } + } else { + Toast.makeText(context, "로그인 데이터 없음", Toast.LENGTH_SHORT).show() + } + } else { + Toast.makeText(context, "로그인 취소", Toast.LENGTH_SHORT).show() + } + } + + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text("Welcome to the App!", style = MaterialTheme.typography.headlineMedium) + Spacer(modifier = Modifier.height(32.dp)) + + Button(onClick = { + val signInRequest = BeginSignInRequest.builder() + .setGoogleIdTokenRequestOptions( + BeginSignInRequest.GoogleIdTokenRequestOptions.builder() + .setSupported(true) + .setServerClientId(BuildConfig.GOOGLE_API_WEB_CLIENT_ID) + .setFilterByAuthorizedAccounts(false) + .build() + ) + .setAutoSelectEnabled(true) + .build() + + signInClient.beginSignIn(signInRequest) + .addOnSuccessListener { result -> + val intentSenderRequest = IntentSenderRequest.Builder(result.pendingIntent).build() + launcher.launch(intentSenderRequest) + } + .addOnFailureListener { + Toast.makeText(context, "로그인 요청 실패", Toast.LENGTH_SHORT).show() + } + }) { + Text("Google 계정으로 로그인") + } + } +} + diff --git a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt new file mode 100644 index 0000000..fa6b441 --- /dev/null +++ b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt @@ -0,0 +1,34 @@ +package com.example.chaining.ui.screen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.google.firebase.Firebase +import com.google.firebase.auth.auth + +@Composable +fun HomeScreen() { + val user = Firebase.auth.currentUser + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text("환영합니다, ${user?.displayName}") + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { + Firebase.auth.signOut() + }) { + Text("로그아웃") + } + } +} \ No newline at end of file From cca980f4c6d227f21970eb952cc7b0674e60dfa1 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 4 Aug 2025 16:45:20 +0900 Subject: [PATCH 003/224] =?UTF-8?q?[Chore]=20API=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 8 ++++++ .../example/chaining/network/AreaService.kt | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 app/src/main/java/com/example/chaining/network/AreaService.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c6c2c04..180691c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) @@ -11,6 +13,10 @@ plugins { id("org.jetbrains.kotlin.plugin.compose") } +val properties = Properties().apply { + load(rootProject.file("local.properties").inputStream()) +} + android { namespace = "com.example.chaining" compileSdk = 34 @@ -22,6 +28,8 @@ android { versionCode = 1 versionName = "1.0" + buildConfigField("String", "DATA_OPEN_API_KEY", properties["DATA_OPEN_API_KEY"].toString()) + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true diff --git a/app/src/main/java/com/example/chaining/network/AreaService.kt b/app/src/main/java/com/example/chaining/network/AreaService.kt new file mode 100644 index 0000000..cac8b11 --- /dev/null +++ b/app/src/main/java/com/example/chaining/network/AreaService.kt @@ -0,0 +1,26 @@ +package com.example.chaining.network + +import com.example.chaining.BuildConfig.DATA +import com.example.tripplan.model.AreaModel +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Query + +// 배포시에는 안전하게 보완 적용 필요 +// 공공 데이터 포탈에서 발급 받은 자신만의 API키를 입력해 주세요. + + +interface AreaService { + @GET("areaCode1?serviceKey=${API_KEY}") + suspend fun getArea( +// @Query("serviceKey") apiKey: String = BuildConfig.API_KEY, + @Query("areaCode") areaCode: String? = "", // 지역 코드 + @Query("numOfRows") numOfRows: Int = 20, + @Query("pageNo") pageNo: Int = 1, + @Query("MobileOS") mobileOS: String = "AND", + @Query("MobileApp") mobileAPP: String = "TripPlan", // 여기를 확인 + @Query("_type") type: String = "json" + ): AreaModel + +} \ No newline at end of file From 7281ded55936cc788be472baa54820be835b1fc5 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 4 Aug 2025 17:12:48 +0900 Subject: [PATCH 004/224] =?UTF-8?q?[Feat]=20=EB=B2=95=EC=A0=95=EB=8F=99=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20API=20Retrofit=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 1 + .../example/chaining/network/AreaService.kt | 29 ++++++++----------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 180691c..d6aad7e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,6 +54,7 @@ android { } buildFeatures { compose = true + buildConfig = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" diff --git a/app/src/main/java/com/example/chaining/network/AreaService.kt b/app/src/main/java/com/example/chaining/network/AreaService.kt index cac8b11..ad2037f 100644 --- a/app/src/main/java/com/example/chaining/network/AreaService.kt +++ b/app/src/main/java/com/example/chaining/network/AreaService.kt @@ -1,26 +1,21 @@ package com.example.chaining.network -import com.example.chaining.BuildConfig.DATA -import com.example.tripplan.model.AreaModel -import okhttp3.ResponseBody -import retrofit2.Call +import com.example.chaining.BuildConfig.DATA_OPEN_API_KEY import retrofit2.http.GET import retrofit2.http.Query -// 배포시에는 안전하게 보완 적용 필요 -// 공공 데이터 포탈에서 발급 받은 자신만의 API키를 입력해 주세요. - +// https://apis.data.go.kr/B551011/KorService2/ldongCode2?serviceKey=DATA_OPEN_API_KEY&numOfRows=1000&lDongListYn=Y&pageNo=1&lDongRegnCd=11&MobileOS=AND&MobileApp=AppTest interface AreaService { - @GET("areaCode1?serviceKey=${API_KEY}") - suspend fun getArea( -// @Query("serviceKey") apiKey: String = BuildConfig.API_KEY, - @Query("areaCode") areaCode: String? = "", // 지역 코드 - @Query("numOfRows") numOfRows: Int = 20, - @Query("pageNo") pageNo: Int = 1, - @Query("MobileOS") mobileOS: String = "AND", - @Query("MobileApp") mobileAPP: String = "TripPlan", // 여기를 확인 - @Query("_type") type: String = "json" - ): AreaModel + @GET("ldongCode2?serviceKey=${DATA_OPEN_API_KEY}") + suspend fun getAreaCodes( + @Query("lDongRegnCd") lDongRegnCd: Int = 11, // 지역 코드 + @Query("PageNo") PageNo: Int = 1, // 불러올 페이지 수 + @Query("numOfRows") numOfRows: Int = 1000, // 불러올 행의 수 + @Query("lDongListYn") lDongListYn: String = "Y", // 목록조회 여부 + @Query("MobileOS") mobileOS: String = "AND", // OS 종류 + @Query("MobileApp") mobileAPP: String = "Chaining", // 앱 명 + @Query("_type") type: String = "json" // 데이터 타입 + ): AreaCodeResponse } \ No newline at end of file From f2d0011372c2109bf77f7cc6753b23cb83b25681 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 4 Aug 2025 17:34:21 +0900 Subject: [PATCH 005/224] =?UTF-8?q?[Feat]=20DTO=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/data/model/AreaCodeResponse.kt | 52 +++++++++++++++++++ .../example/chaining/network/AreaService.kt | 1 + 2 files changed, 53 insertions(+) create mode 100644 app/src/main/java/com/example/chaining/data/model/AreaCodeResponse.kt diff --git a/app/src/main/java/com/example/chaining/data/model/AreaCodeResponse.kt b/app/src/main/java/com/example/chaining/data/model/AreaCodeResponse.kt new file mode 100644 index 0000000..d83f4dc --- /dev/null +++ b/app/src/main/java/com/example/chaining/data/model/AreaCodeResponse.kt @@ -0,0 +1,52 @@ +package com.example.chaining.data.model + + +import com.google.gson.annotations.SerializedName + +data class AreaCodeResponse( + @SerializedName("response") + val response: Response +) { + data class Response( + @SerializedName("body") + val body: Body, + @SerializedName("header") + val header: Header + ) { + data class Body( + @SerializedName("items") + val items: Items, + @SerializedName("numOfRows") + val numOfRows: Int, + @SerializedName("pageNo") + val pageNo: Int, + @SerializedName("totalCount") + val totalCount: Int + ) { + data class Items( + @SerializedName("item") + val item: List + ) { + data class Item( + @SerializedName("lDongRegnCd") + val lDongRegnCd: String, + @SerializedName("lDongRegnNm") + val lDongRegnNm: String, + @SerializedName("lDongSignguCd") + val lDongSignguCd: String, + @SerializedName("lDongSignguNm") + val lDongSignguNm: String, + @SerializedName("rnum") + val rnum: Int + ) + } + } + + data class Header( + @SerializedName("resultCode") + val resultCode: String, + @SerializedName("resultMsg") + val resultMsg: String + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/network/AreaService.kt b/app/src/main/java/com/example/chaining/network/AreaService.kt index ad2037f..8ef82d3 100644 --- a/app/src/main/java/com/example/chaining/network/AreaService.kt +++ b/app/src/main/java/com/example/chaining/network/AreaService.kt @@ -1,6 +1,7 @@ package com.example.chaining.network import com.example.chaining.BuildConfig.DATA_OPEN_API_KEY +import com.example.chaining.data.model.AreaCodeResponse import retrofit2.http.GET import retrofit2.http.Query From c700a1014f85aee5df57d273e8078b225183d789 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 4 Aug 2025 17:57:09 +0900 Subject: [PATCH 006/224] =?UTF-8?q?[Feat]=20API=20=ED=82=A4=20=ED=95=98?= =?UTF-8?q?=EB=93=9C=EC=BD=94=EB=94=A9=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?Repository=20Layer=20=ED=8C=8C=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/data/model/AreaCodeResponse.kt | 21 +++++++++---------- .../data/repository/AreaRepository.kt | 17 +++++++++++++++ .../example/chaining/network/AreaService.kt | 4 ++-- 3 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt diff --git a/app/src/main/java/com/example/chaining/data/model/AreaCodeResponse.kt b/app/src/main/java/com/example/chaining/data/model/AreaCodeResponse.kt index d83f4dc..9287cf3 100644 --- a/app/src/main/java/com/example/chaining/data/model/AreaCodeResponse.kt +++ b/app/src/main/java/com/example/chaining/data/model/AreaCodeResponse.kt @@ -1,21 +1,20 @@ package com.example.chaining.data.model - import com.google.gson.annotations.SerializedName data class AreaCodeResponse( @SerializedName("response") - val response: Response + val response: AreaCodeResponse ) { - data class Response( + data class AreaCodeResponse( @SerializedName("body") - val body: Body, + val body: AreaCodeBody, @SerializedName("header") - val header: Header + val header: AreaCodeHeader ) { - data class Body( + data class AreaCodeBody( @SerializedName("items") - val items: Items, + val items: AreaCodeItems, @SerializedName("numOfRows") val numOfRows: Int, @SerializedName("pageNo") @@ -23,11 +22,11 @@ data class AreaCodeResponse( @SerializedName("totalCount") val totalCount: Int ) { - data class Items( + data class AreaCodeItems( @SerializedName("item") - val item: List + val item: List ) { - data class Item( + data class AreaCodeItem( @SerializedName("lDongRegnCd") val lDongRegnCd: String, @SerializedName("lDongRegnNm") @@ -42,7 +41,7 @@ data class AreaCodeResponse( } } - data class Header( + data class AreaCodeHeader( @SerializedName("resultCode") val resultCode: String, @SerializedName("resultMsg") diff --git a/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt b/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt new file mode 100644 index 0000000..16527c8 --- /dev/null +++ b/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt @@ -0,0 +1,17 @@ +package com.example.chaining.data.repository + +import com.example.chaining.BuildConfig +import com.example.chaining.data.model.AreaCodeResponse +import com.example.chaining.network.AreaService +import jakarta.inject.Inject + +class AreaRepository @Inject constructor( + private val api: AreaService +) { + suspend fun fetchAreaCodes(): List { + val response = api.getAreaCodes( + serviceKey = BuildConfig.DATA_OPEN_API_KEY + "&_type=json" + ) + return response.response.body.items.item + } +} diff --git a/app/src/main/java/com/example/chaining/network/AreaService.kt b/app/src/main/java/com/example/chaining/network/AreaService.kt index 8ef82d3..3204baa 100644 --- a/app/src/main/java/com/example/chaining/network/AreaService.kt +++ b/app/src/main/java/com/example/chaining/network/AreaService.kt @@ -1,6 +1,5 @@ package com.example.chaining.network -import com.example.chaining.BuildConfig.DATA_OPEN_API_KEY import com.example.chaining.data.model.AreaCodeResponse import retrofit2.http.GET import retrofit2.http.Query @@ -8,8 +7,9 @@ import retrofit2.http.Query // https://apis.data.go.kr/B551011/KorService2/ldongCode2?serviceKey=DATA_OPEN_API_KEY&numOfRows=1000&lDongListYn=Y&pageNo=1&lDongRegnCd=11&MobileOS=AND&MobileApp=AppTest interface AreaService { - @GET("ldongCode2?serviceKey=${DATA_OPEN_API_KEY}") + @GET("ldongCode2") suspend fun getAreaCodes( + @Query("serviceKey", encoded = true) serviceKey: String, // API 키 @Query("lDongRegnCd") lDongRegnCd: Int = 11, // 지역 코드 @Query("PageNo") PageNo: Int = 1, // 불러올 페이지 수 @Query("numOfRows") numOfRows: Int = 1000, // 불러올 행의 수 From eaa99b17a626ad4172ac849a6ec890a80f79a08b Mon Sep 17 00:00:00 2001 From: hi2242 Date: Wed, 6 Aug 2025 21:59:49 +0900 Subject: [PATCH 007/224] =?UTF-8?q?[Feat]=20ViewModel=20Layer=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20Screen=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 1 + .../java/com/example/chaining/MainActivity.kt | 39 ++----------------- .../data/repository/AreaRepository.kt | 2 +- .../chaining/ui/theme/screen/AreaScreen.kt | 26 +++++++++++++ .../chaining/viewmodel/AreaViewModel.kt | 34 ++++++++++++++++ 5 files changed, 65 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt create mode 100644 app/src/main/java/com/example/chaining/viewmodel/AreaViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d6aad7e..2ecb371 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -75,6 +75,7 @@ dependencies { // Hilt 의존성 주입 (DI) 라이브러리 사용 implementation("com.google.dagger:hilt-android:2.55") kapt("com.google.dagger:hilt-android-compiler:2.55") + implementation("androidx.hilt:hilt-navigation-compose:1.2.0") // Room (로컬 DB) 의존성 주입 implementation("androidx.room:room-runtime:2.6.1") diff --git a/app/src/main/java/com/example/chaining/MainActivity.kt b/app/src/main/java/com/example/chaining/MainActivity.kt index 15772cf..8659a34 100644 --- a/app/src/main/java/com/example/chaining/MainActivity.kt +++ b/app/src/main/java/com/example/chaining/MainActivity.kt @@ -4,47 +4,14 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.example.chaining.ui.theme.chainingTheme +import com.example.chaining.ui.theme.screen.AreaScreen class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { - chainingTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - greeting( - name = "Android", - modifier = Modifier.padding(innerPadding), - ) - } - } + AreaScreen() } } -} - -@Composable -fun greeting( - name: String, - modifier: Modifier = Modifier, -) { - Text( - text = "Hi $name!", - modifier = modifier, - ) -} - -@Preview(showBackground = true) -@Composable -fun greetingPreview() { - chainingTheme { - greeting("Android") - } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt b/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt index 16527c8..376e0dd 100644 --- a/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt +++ b/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt @@ -3,7 +3,7 @@ package com.example.chaining.data.repository import com.example.chaining.BuildConfig import com.example.chaining.data.model.AreaCodeResponse import com.example.chaining.network.AreaService -import jakarta.inject.Inject +import javax.inject.Inject class AreaRepository @Inject constructor( private val api: AreaService diff --git a/app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt b/app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt new file mode 100644 index 0000000..4282be3 --- /dev/null +++ b/app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt @@ -0,0 +1,26 @@ +package com.example.chaining.ui.theme.screen + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.example.chaining.viewmodel.AreaViewModel + +@Composable +fun AreaScreen(viewModel: AreaViewModel = hiltViewModel()) { + val areaCodes by viewModel.areaCodes + + LazyColumn { + items(areaCodes) { area -> + Text( + text = "${area.lDongRegnNm} (${area.lDongRegnCd})", + modifier = Modifier.padding(8.dp) + ) + } + } +} diff --git a/app/src/main/java/com/example/chaining/viewmodel/AreaViewModel.kt b/app/src/main/java/com/example/chaining/viewmodel/AreaViewModel.kt new file mode 100644 index 0000000..5b13356 --- /dev/null +++ b/app/src/main/java/com/example/chaining/viewmodel/AreaViewModel.kt @@ -0,0 +1,34 @@ +package com.example.chaining.viewmodel + +import android.util.Log +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.chaining.data.model.AreaCodeResponse.AreaCodeResponse.AreaCodeBody.AreaCodeItems.AreaCodeItem +import com.example.chaining.data.repository.AreaRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@HiltViewModel +class AreaViewModel @Inject constructor( + private val repository: AreaRepository +) : ViewModel() { + + // 상태 보관 (외부에는 읽기 전용으로 제공) + private val _areaCodes = mutableStateOf>(emptyList()) + val areaCodes: State> = _areaCodes + + init { + viewModelScope.launch { + try { + // Repository에서 데이터 가져옴 + _areaCodes.value = repository.fetchAreaCodes() + } catch (e: Exception) { + Log.e("AreaViewModel", "지역 코드 로드 실패", e) + } + } + } +} From 1097c5d8cc6205d55aaaa901da10470434b960c3 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Wed, 6 Aug 2025 22:29:17 +0900 Subject: [PATCH 008/224] =?UTF-8?q?[Feat]=20Area=20API=20DI=20Provider=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/chaining/di/AreaModule.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/src/main/java/com/example/chaining/di/AreaModule.kt diff --git a/app/src/main/java/com/example/chaining/di/AreaModule.kt b/app/src/main/java/com/example/chaining/di/AreaModule.kt new file mode 100644 index 0000000..04fd95b --- /dev/null +++ b/app/src/main/java/com/example/chaining/di/AreaModule.kt @@ -0,0 +1,22 @@ +package com.example.chaining.di + +import com.example.chaining.network.AreaService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +@Module +@InstallIn(SingletonComponent::class) +object AreaModule { + @Provides + fun provideAreaApi(): AreaService { + return Retrofit.Builder() + .baseUrl("https://apis.data.go.kr/B551011/KorService2/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(AreaService::class.java) + } +} From 42ab3b38c520adf33bdbcd0fb1ff31a06da3dd85 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Thu, 7 Aug 2025 15:35:27 +0900 Subject: [PATCH 009/224] =?UTF-8?q?[Fix]=20Hilt=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 2 +- app/src/main/java/com/example/chaining/MainActivity.kt | 2 ++ .../java/com/example/chaining/ui/theme/screen/AreaScreen.kt | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2ecb371..433acba 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,7 +19,7 @@ val properties = Properties().apply { android { namespace = "com.example.chaining" - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "com.example.chaining" diff --git a/app/src/main/java/com/example/chaining/MainActivity.kt b/app/src/main/java/com/example/chaining/MainActivity.kt index 8659a34..068601c 100644 --- a/app/src/main/java/com/example/chaining/MainActivity.kt +++ b/app/src/main/java/com/example/chaining/MainActivity.kt @@ -5,7 +5,9 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import com.example.chaining.ui.theme.screen.AreaScreen +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt b/app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt index 4282be3..f249e56 100644 --- a/app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt @@ -1,5 +1,6 @@ package com.example.chaining.ui.theme.screen +import android.util.Log import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -17,6 +18,7 @@ fun AreaScreen(viewModel: AreaViewModel = hiltViewModel()) { LazyColumn { items(areaCodes) { area -> + Log.d(area.lDongRegnNm, area.lDongRegnCd) Text( text = "${area.lDongRegnNm} (${area.lDongRegnCd})", modifier = Modifier.padding(8.dp) From 7e93196084d9de2bc51d83574ba3df1e5c11cea2 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Fri, 8 Aug 2025 18:13:06 +0900 Subject: [PATCH 010/224] =?UTF-8?q?[Fix]=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EB=8C=80=EC=86=8C=EB=AC=B8=EC=9E=90=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20AreaScreen=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/repository/AreaRepository.kt | 2 +- .../example/chaining/network/AreaService.kt | 2 +- .../chaining/ui/theme/screen/AreaScreen.kt | 56 ++++++++++++++++--- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt b/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt index 376e0dd..8fd6c6d 100644 --- a/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt +++ b/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt @@ -10,7 +10,7 @@ class AreaRepository @Inject constructor( ) { suspend fun fetchAreaCodes(): List { val response = api.getAreaCodes( - serviceKey = BuildConfig.DATA_OPEN_API_KEY + "&_type=json" + serviceKey = BuildConfig.DATA_OPEN_API_KEY ) return response.response.body.items.item } diff --git a/app/src/main/java/com/example/chaining/network/AreaService.kt b/app/src/main/java/com/example/chaining/network/AreaService.kt index 3204baa..29b6e78 100644 --- a/app/src/main/java/com/example/chaining/network/AreaService.kt +++ b/app/src/main/java/com/example/chaining/network/AreaService.kt @@ -11,7 +11,7 @@ interface AreaService { suspend fun getAreaCodes( @Query("serviceKey", encoded = true) serviceKey: String, // API 키 @Query("lDongRegnCd") lDongRegnCd: Int = 11, // 지역 코드 - @Query("PageNo") PageNo: Int = 1, // 불러올 페이지 수 + @Query("pageNo") PageNo: Int = 1, // 불러올 페이지 수 @Query("numOfRows") numOfRows: Int = 1000, // 불러올 행의 수 @Query("lDongListYn") lDongListYn: String = "Y", // 목록조회 여부 @Query("MobileOS") mobileOS: String = "AND", // OS 종류 diff --git a/app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt b/app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt index f249e56..a6d1f21 100644 --- a/app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt @@ -1,13 +1,20 @@ package com.example.chaining.ui.theme.screen -import android.util.Log +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.example.chaining.viewmodel.AreaViewModel @@ -16,13 +23,46 @@ import com.example.chaining.viewmodel.AreaViewModel fun AreaScreen(viewModel: AreaViewModel = hiltViewModel()) { val areaCodes by viewModel.areaCodes - LazyColumn { - items(areaCodes) { area -> - Log.d(area.lDongRegnNm, area.lDongRegnCd) - Text( - text = "${area.lDongRegnNm} (${area.lDongRegnCd})", - modifier = Modifier.padding(8.dp) - ) + Column(modifier = Modifier.padding(16.dp)) { + // 테이블 헤더 + Row( + modifier = Modifier + .fillMaxWidth() + .border(1.dp, Color.Black) + .padding(8.dp) + ) { + TableCell("No.", Modifier.weight(0.5f)) + TableCell("시/도", Modifier.weight(1f)) + TableCell("구 이름", Modifier.weight(1f)) + TableCell("구 코드", Modifier.weight(1f)) + } + + Spacer(modifier = Modifier.height(4.dp)) + + // 테이블 내용 + LazyColumn { + items(areaCodes) { area -> + Row( + modifier = Modifier + .fillMaxWidth() + .border(0.5.dp, Color.Gray) + .padding(8.dp) + ) { + TableCell(area.rnum.toString(), Modifier.weight(0.5f)) + TableCell(area.lDongRegnNm, Modifier.weight(1f)) + TableCell(area.lDongSignguNm, Modifier.weight(1f)) + TableCell(area.lDongSignguCd.toString(), Modifier.weight(1f)) + } + } } } } + +@Composable +fun TableCell(text: String, modifier: Modifier = Modifier) { + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + modifier = modifier.padding(4.dp) + ) +} From 84831ceb614509bddc6235f09e414a98d8de9c70 Mon Sep 17 00:00:00 2001 From: lhs2257 Date: Fri, 8 Aug 2025 20:27:18 +0900 Subject: [PATCH 011/224] =?UTF-8?q?[Feat]=20Splash=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/misc.xml | 1 - app/build.gradle.kts | 3 + .../java/com/example/chaining/MainActivity.kt | 4 ++ .../chaining/ui/component/SplashAnimation.kt | 34 ++++++++++ .../chaining/ui/navigation/NavGraph.kt | 16 +++++ .../example/chaining/ui/screen/HomeScreen.kt | 18 +++++ .../chaining/ui/screen/SplashScreen.kt | 64 ++++++++++++++++++ app/src/main/res/drawable/chain.png | Bin 0 -> 2490 bytes 8 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/example/chaining/ui/component/SplashAnimation.kt create mode 100644 app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt create mode 100644 app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt create mode 100644 app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt create mode 100644 app/src/main/res/drawable/chain.png diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ad17cb..8978d23 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c6c2c04..7699b31 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -77,6 +77,9 @@ dependencies { implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1") + // Navigation 라이브러리 의존성 주입 + implementation ("androidx.navigation:navigation-compose:2.7.7") + implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) diff --git a/app/src/main/java/com/example/chaining/MainActivity.kt b/app/src/main/java/com/example/chaining/MainActivity.kt index 15772cf..923d5b9 100644 --- a/app/src/main/java/com/example/chaining/MainActivity.kt +++ b/app/src/main/java/com/example/chaining/MainActivity.kt @@ -11,6 +11,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.compose.rememberNavController +import com.example.chaining.ui.navigation.ChainingNavGraph import com.example.chaining.ui.theme.chainingTheme class MainActivity : ComponentActivity() { @@ -19,7 +21,9 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { chainingTheme { + val navController = rememberNavController() Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + ChainingNavGraph(navController = navController) greeting( name = "Android", modifier = Modifier.padding(innerPadding), diff --git a/app/src/main/java/com/example/chaining/ui/component/SplashAnimation.kt b/app/src/main/java/com/example/chaining/ui/component/SplashAnimation.kt new file mode 100644 index 0000000..928e0f1 --- /dev/null +++ b/app/src/main/java/com/example/chaining/ui/component/SplashAnimation.kt @@ -0,0 +1,34 @@ +package com.example.chaining.ui.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.example.chaining.R +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween + +@Composable +fun SplashAnimation() { + var startAnimation by remember { mutableStateOf(false) } + + val chainWidth by animateDpAsState( + targetValue = if (startAnimation) 200.dp else 0.dp, + animationSpec = tween(durationMillis = 1000) + ) + + LaunchedEffect(true) { + startAnimation = true + } + + Image( + painter = painterResource(id = R.drawable.chain), + contentDescription = "Chain", + modifier = Modifier + .size(64.dp) + .height(20.dp) + .width(chainWidth) + ) +} diff --git a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt new file mode 100644 index 0000000..22b24c3 --- /dev/null +++ b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt @@ -0,0 +1,16 @@ +package com.example.chaining.ui.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.example.chaining.ui.screen.SplashScreen +import com.example.chaining.ui.screen.HomeScreen + +@Composable +fun ChainingNavGraph(navController: NavHostController) { + NavHost(navController = navController, startDestination = "splash") { + composable("splash") { SplashScreen(navController) } + composable("home") { HomeScreen() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt new file mode 100644 index 0000000..b380195 --- /dev/null +++ b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt @@ -0,0 +1,18 @@ +package com.example.chaining.ui.screen + +import androidx.compose.runtime.Composable +import androidx.compose.material3.Text +import androidx.compose.foundation.layout.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.sp + +@Composable +fun HomeScreen() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text("Home Screen", fontSize = 32.sp) + } +} diff --git a/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt new file mode 100644 index 0000000..68db8da --- /dev/null +++ b/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt @@ -0,0 +1,64 @@ +package com.example.chaining.ui.screen + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.sp +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.material3.Text +import androidx.navigation.NavController +import kotlinx.coroutines.delay +import androidx.compose.animation.core.FastOutSlowInEasing +import com.example.chaining.ui.component.SplashAnimation + +@Composable +fun SplashScreen(navController: NavController) { + val offsetX = remember { Animatable(300f) } + val chainVisible = remember { mutableStateOf(false) } + + LaunchedEffect(true) { + offsetX.animateTo( + targetValue = 0f, + animationSpec = tween(durationMillis = 800, easing = FastOutSlowInEasing) + ) + delay(300) + chainVisible.value = true + delay(1500) + navController.navigate("home") { + popUpTo("splash") { inclusive = true } + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + if (chainVisible.value) { + SplashAnimation() + Spacer(modifier = Modifier.height(8.dp)) // 이미지와 텍스트 간격 + } + + Text( + text = "Chaining", + fontSize = 50.sp, + fontWeight = FontWeight.Bold, + color = Color.Black, + modifier = Modifier.offset { IntOffset(offsetX.value.toInt(), 0) } + ) + } + + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/chain.png b/app/src/main/res/drawable/chain.png new file mode 100644 index 0000000000000000000000000000000000000000..8b7adaa04d8094597ff63dc512ace2120c5168f4 GIT binary patch literal 2490 zcmV;r2}SmaP)w?Lv$L7?SgHXR6;1zYG!rL3#OnCK5< zG+Y{$tgsEb63YErLIa6h5-+PI67h!{69S~WZ0Nc|A_OY5%MWgbg{-KFwWV_FP(W7O zDm~^I&*=1U&NtsT^UVz3neR#7P-f1h=RAGS|%6M&Oa`19)kUIS1r(}u_p*ujkeZUYc5Bm6equ8jbo!Mj|O1P2uV*ToS_v zGH%|yDbAliACs=GE?0Rt;cMoUcaZ0Zq)Ntv2M@%K9XrINNt2!$ekMfE_=qNJoG=_Zhif!5YmV>zrxvm~aOCGQYP zRWq8Ko5heJLtO97nfC12v&QncPf_yx4L||%2+yag8D7bV%h<7F#r^yDwPjwscu|)p zZ-%W%wlqVP&6_tHck~)F%Xg3mh~$b4%D8dk#FHmawB;{cxKK_1BUp||F3E_?wQJY3 zbs3-L|0LuSmF;TIY2Co!29N;AlyN!@gQvS7gi zwS3NZR^ej+x)K{Xdi3a|R$HIQ;HK^uYQC*ow`%JoNhC{2{|8uxHEM?(J$f`L8AU}! z86ksamY$v-FB?Oik64330PTqloHuWtBN_Yl?GuHCg~`Zx4B)d~$)Lv3_GJP!M^o2Z zun6jZ%S3;F|Dd$||B@w3bm`}(O2(~Qw?tW4nTd=Zn;ag>)CP8T zcDf>C`SRt)eL`PfpO`Xbikj#5sgS{a5Ka9KL*g%C5h@cKS5Qze=r2AMGUC{^YnS8P z;+70r3X;rGFT*0dl-M|~VEiD1IC$`2HpsAQ|6$aSbjfhi5Fg2)gTv=wGa&%Pnl)?E zCc`j?_(=xk4OoZ@7Y+AB2GcEFy7Z|sXlC(|j6VVx4hu0mS>tPKYmG;#Ix<$QSn*UD z9149TquXYW#Y#UlgXwkUG&VN+MFw~M)d(n_lV{^EN+aKka^JztU>xH!O~&@^+a2@L zsyk=S95-YC_of8|Sy1Lr&q_4qX4#@@?%y;QR^q87o(=)P2r5F76gORi42tl2?;o0q_<1Im7)0 z4!)B?*|TR)+GNz%*NfiX-q?A#@*U6Ntwf28j^-;F6gsJ;OGX@;dKSV$6zKv+`auST zUg&<1(c9arOS2pnVz=7R*|TT+LPmTJi5vVhZ`Y;!HY^M>+^wyCYVjXU} zV_`baShsGSF8$YG5oW64w;4*+2QuE5j>oi|y4E5jIb1T_hN6aBgGSZ)z{x)4fsFXo z<1c$5WxInDTT4reDId@Dtw5oS{QtkkaX2x7QPo_At0>iIVXITVf`WqB)n*eo@mEz< z>GFPP({mE#Y6BT4SO>1Q#*7)`MKg4m58$erk7gEL`gg*SBS%biUV%k;EwOPECr&il znmDhdpc$?%JD{( z0T$s+^`?>zSFc{p85xz%9)(>&H5i)E;>G~Tl~uNfsi-?&+s&{HQ8$DzirO=#8A4vp zr$4b(7<8bsdi82ALsnG|$q)d(3k%Vi*hoI}<6%;7Hmv^(vp~l0Y>ga!Lw%3QwCrUN zVNJ)ptC%fj%9qeGNk)gf_GTs8)CThj1|1k?o8h({{F%CS-?<^9$NinYhjKG(E#CHG z*mG$*$&_Y13W~37I@qT+ocAT}-o5Kcii4&Mic#nk7SE$* Date: Sat, 9 Aug 2025 15:20:33 +0900 Subject: [PATCH 012/224] =?UTF-8?q?[Feat]=20=ED=99=94=EB=A9=B4=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=95=EC=9D=98=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=ED=99=94=EB=A9=B4=20=EC=88=9C=EC=84=9C?= =?UTF-8?q?=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/chaining/MainActivity.kt | 51 +++---------------- .../chaining/ui/navigation/NavGraph.kt | 35 ++++++++++--- .../example/chaining/ui/navigation/Screen.kt | 8 +++ .../ui/{theme => }/screen/AreaScreen.kt | 2 +- .../example/chaining/ui/screen/HomeScreen.kt | 14 +++-- 5 files changed, 53 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/example/chaining/ui/navigation/Screen.kt rename app/src/main/java/com/example/chaining/ui/{theme => }/screen/AreaScreen.kt (98%) diff --git a/app/src/main/java/com/example/chaining/MainActivity.kt b/app/src/main/java/com/example/chaining/MainActivity.kt index 1e84a05..09d20ec 100644 --- a/app/src/main/java/com/example/chaining/MainActivity.kt +++ b/app/src/main/java/com/example/chaining/MainActivity.kt @@ -7,22 +7,11 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.compose.rememberNavController -import com.example.chaining.ui.navigation.ChainingNavGraph +import com.example.chaining.ui.navigation.NavGraph import com.example.chaining.ui.theme.chainingTheme -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import com.example.chaining.ui.login.LoginScreen -import com.example.chaining.ui.screen.HomeScreen -import com.google.firebase.Firebase import com.google.firebase.FirebaseApp -import com.google.firebase.auth.auth import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -32,44 +21,16 @@ class MainActivity : ComponentActivity() { FirebaseApp.initializeApp(this) enableEdgeToEdge() setContent { -// var isLoggedIn by remember { mutableStateOf(Firebase.auth.currentUser != null) } -// -// if (isLoggedIn) { -// HomeScreen() -// } else { -// LoginScreen { -// isLoggedIn = true -// } -// } + chainingTheme { val navController = rememberNavController() Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - ChainingNavGraph(navController = navController) - greeting( - name = "Android", - modifier = Modifier.padding(innerPadding), + NavGraph( + navController = navController, + modifier = Modifier.padding(innerPadding) ) } } } } -} - -@Composable -fun greeting( - name: String, - modifier: Modifier = Modifier, -) { - Text( - text = "Hello $name!", - modifier = modifier, - ) -} - -@Preview(showBackground = true) -@Composable -fun greetingPreview() { - chainingTheme { - greeting("Android") - } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt index 22b24c3..1475400 100644 --- a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt +++ b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt @@ -1,16 +1,39 @@ package com.example.chaining.ui.navigation import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import com.example.chaining.ui.screen.SplashScreen +import com.example.chaining.ui.login.LoginScreen +import com.example.chaining.ui.screen.AreaScreen import com.example.chaining.ui.screen.HomeScreen +import com.example.chaining.ui.screen.SplashScreen @Composable -fun ChainingNavGraph(navController: NavHostController) { - NavHost(navController = navController, startDestination = "splash") { - composable("splash") { SplashScreen(navController) } - composable("home") { HomeScreen() } +fun NavGraph(navController: NavHostController, modifier: Modifier = Modifier) { + NavHost( + navController = navController, + startDestination = "splash", + modifier = modifier + ) { + composable("splash") { + SplashScreen(navController) + } + composable("login") { + LoginScreen { + navController.navigate("home") { + popUpTo("login") { inclusive = true } + } + } + } + composable("home") { + HomeScreen( + onTableClick = { navController.navigate("area") } + ) + } + composable("area") { + AreaScreen() + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt b/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt new file mode 100644 index 0000000..44510c7 --- /dev/null +++ b/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt @@ -0,0 +1,8 @@ +package com.example.chaining.ui.navigation + +sealed class Screen(val route: String) { + object Splash : Screen("splash") + object Login : Screen("login") + object Home : Screen("home") + object Area : Screen("area") +} \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/AreaScreen.kt similarity index 98% rename from app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt rename to app/src/main/java/com/example/chaining/ui/screen/AreaScreen.kt index a6d1f21..394ed14 100644 --- a/app/src/main/java/com/example/chaining/ui/theme/screen/AreaScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/AreaScreen.kt @@ -1,4 +1,4 @@ -package com.example.chaining.ui.theme.screen +package com.example.chaining.ui.screen import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt index fa6b441..ebf92ec 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt @@ -16,18 +16,22 @@ import com.google.firebase.Firebase import com.google.firebase.auth.auth @Composable -fun HomeScreen() { +fun HomeScreen(onTableClick: () -> Unit) { val user = Firebase.auth.currentUser Column( - modifier = Modifier.fillMaxSize().padding(16.dp), + modifier = Modifier + .fillMaxSize() + .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("환영합니다, ${user?.displayName}") Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { - Firebase.auth.signOut() - }) { + Button(onClick = onTableClick) { + Text("테이블 보기") + } + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { Firebase.auth.signOut() }) { Text("로그아웃") } } From 99da91368764777beda3a37d3d14048db8093f5b Mon Sep 17 00:00:00 2001 From: hi2242 Date: Sat, 9 Aug 2025 15:36:01 +0900 Subject: [PATCH 013/224] =?UTF-8?q?[Fix]=20SplashScreen.kt=20=EB=84=A4?= =?UTF-8?q?=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/ui/screen/SplashScreen.kt | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt index 68db8da..c4b9f8c 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt @@ -1,22 +1,33 @@ package com.example.chaining.ui.screen import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.sp import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight -import androidx.compose.material3.Text +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import kotlinx.coroutines.delay -import androidx.compose.animation.core.FastOutSlowInEasing import com.example.chaining.ui.component.SplashAnimation +import com.google.firebase.Firebase +import com.google.firebase.auth.auth +import kotlinx.coroutines.delay @Composable fun SplashScreen(navController: NavController) { @@ -31,8 +42,15 @@ fun SplashScreen(navController: NavController) { delay(300) chainVisible.value = true delay(1500) - navController.navigate("home") { - popUpTo("splash") { inclusive = true } + val isLoggedIn = Firebase.auth.currentUser != null + if (isLoggedIn) { + navController.navigate("home") { + popUpTo("splash") { inclusive = true } + } + } else { + navController.navigate("login") { + popUpTo("splash") { inclusive = true } + } } } From e5ef718c195af406fc457c6b6ee25899a6552fac Mon Sep 17 00:00:00 2001 From: lhs2257 Date: Sun, 10 Aug 2025 14:47:30 +0900 Subject: [PATCH 014/224] =?UTF-8?q?[Design]=20SplashScreen.kt=20=EB=82=B4?= =?UTF-8?q?=20=ED=85=8D=EC=8A=A4=ED=8A=B8=201=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/ui/screen/SplashScreen.kt | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt index c4b9f8c..1cebaa4 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.IntOffset @@ -28,20 +29,33 @@ import com.example.chaining.ui.component.SplashAnimation import com.google.firebase.Firebase import com.google.firebase.auth.auth import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @Composable fun SplashScreen(navController: NavController) { - val offsetX = remember { Animatable(300f) } + val offsetX = remember { Animatable(300f) } // 슬라이드 + val alpha = remember { Animatable(0f) } // 페이드 인 val chainVisible = remember { mutableStateOf(false) } LaunchedEffect(true) { - offsetX.animateTo( - targetValue = 0f, - animationSpec = tween(durationMillis = 800, easing = FastOutSlowInEasing) - ) - delay(300) + // 슬라이드 인 + 페이드 인 동시 실행 + launch { + offsetX.animateTo( + targetValue = 0f, + animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing) + ) + } + launch { + alpha.animateTo( + targetValue = 1f, + animationSpec = tween(durationMillis = 1500, easing = FastOutSlowInEasing) + ) + } + + delay(800) chainVisible.value = true delay(1500) + val isLoggedIn = Firebase.auth.currentUser != null if (isLoggedIn) { navController.navigate("home") { @@ -66,7 +80,7 @@ fun SplashScreen(navController: NavController) { ) { if (chainVisible.value) { SplashAnimation() - Spacer(modifier = Modifier.height(8.dp)) // 이미지와 텍스트 간격 + Spacer(modifier = Modifier.height(8.dp)) } Text( @@ -74,9 +88,10 @@ fun SplashScreen(navController: NavController) { fontSize = 50.sp, fontWeight = FontWeight.Bold, color = Color.Black, - modifier = Modifier.offset { IntOffset(offsetX.value.toInt(), 0) } + modifier = Modifier + .offset { IntOffset(offsetX.value.toInt(), 0) } + .alpha(alpha.value) ) } - } -} \ No newline at end of file +} From c1ecd30b833a712fe872c064c0722325244c2426 Mon Sep 17 00:00:00 2001 From: lhs2257 Date: Sun, 10 Aug 2025 16:50:54 +0900 Subject: [PATCH 015/224] =?UTF-8?q?[Design]=20SplashScreen.kt/SplashAnimat?= =?UTF-8?q?ion.kt=20=EB=82=B4=20=ED=85=8D=EC=8A=A4=ED=8A=B8/=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=202=EB=8B=A8=EA=B3=84=20=EC=95=A0=EB=8B=88?= =?UTF-8?q?=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/ui/component/SplashAnimation.kt | 34 +++++++++++-------- .../chaining/ui/screen/SplashScreen.kt | 26 +++++++++----- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/example/chaining/ui/component/SplashAnimation.kt b/app/src/main/java/com/example/chaining/ui/component/SplashAnimation.kt index 928e0f1..cda66e6 100644 --- a/app/src/main/java/com/example/chaining/ui/component/SplashAnimation.kt +++ b/app/src/main/java/com/example/chaining/ui/component/SplashAnimation.kt @@ -1,34 +1,38 @@ package com.example.chaining.ui.component +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.size import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.example.chaining.R -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.tween +import kotlinx.coroutines.launch @Composable fun SplashAnimation() { - var startAnimation by remember { mutableStateOf(false) } - - val chainWidth by animateDpAsState( - targetValue = if (startAnimation) 200.dp else 0.dp, - animationSpec = tween(durationMillis = 1000) - ) + val offsetY = remember { Animatable(0f) } // 아래 -> 위 - LaunchedEffect(true) { - startAnimation = true + LaunchedEffect(Unit) { + launch { + offsetY.animateTo( + targetValue = -50f, + animationSpec = tween(durationMillis = 200) + ) + } } Image( painter = painterResource(id = R.drawable.chain), contentDescription = "Chain", modifier = Modifier - .size(64.dp) - .height(20.dp) - .width(chainWidth) + .size(70.dp) + .graphicsLayer { + translationY = offsetY.value + } ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt index 1cebaa4..4fac1b0 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/SplashScreen.kt @@ -7,9 +7,7 @@ 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.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -22,7 +20,6 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.example.chaining.ui.component.SplashAnimation @@ -33,12 +30,14 @@ import kotlinx.coroutines.launch @Composable fun SplashScreen(navController: NavController) { - val offsetX = remember { Animatable(300f) } // 슬라이드 + val offsetX = remember { Animatable(300f) } // X축 슬라이드인 용도 val alpha = remember { Animatable(0f) } // 페이드 인 - val chainVisible = remember { mutableStateOf(false) } + val offsetY = remember { Animatable(0f) } // Y축 슬라이드인 용도 + val chainVisible = remember { mutableStateOf(false) } // 체인 이미지 용도 + val textSlideDownStart = remember { mutableStateOf(false) } // 2단계 시작 용도 - LaunchedEffect(true) { - // 슬라이드 인 + 페이드 인 동시 실행 + LaunchedEffect(Unit) { + // 1단계: 오른쪽 -> 중앙 슬라이드 + 페이드인 launch { offsetX.animateTo( targetValue = 0f, @@ -53,7 +52,17 @@ fun SplashScreen(navController: NavController) { } delay(800) + + // 2단계: 중앙 -> 아래쪽 슬라이드 + textSlideDownStart.value = true + launch { + offsetY.animateTo( + 70f, + animationSpec = tween(200, easing = FastOutSlowInEasing) + ) + } chainVisible.value = true + delay(1500) val isLoggedIn = Firebase.auth.currentUser != null @@ -80,7 +89,6 @@ fun SplashScreen(navController: NavController) { ) { if (chainVisible.value) { SplashAnimation() - Spacer(modifier = Modifier.height(8.dp)) } Text( @@ -89,7 +97,7 @@ fun SplashScreen(navController: NavController) { fontWeight = FontWeight.Bold, color = Color.Black, modifier = Modifier - .offset { IntOffset(offsetX.value.toInt(), 0) } + .offset { IntOffset(offsetX.value.toInt(), offsetY.value.toInt()) } .alpha(alpha.value) ) } From cd9c1f620260892df0a22824d1744fcc229509e7 Mon Sep 17 00:00:00 2001 From: lhs2257 Date: Sun, 17 Aug 2025 12:50:16 +0900 Subject: [PATCH 016/224] =?UTF-8?q?[Design]=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84=ED=8A=BC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/chaining/ui/login/LoginScreen.kt | 132 +++++++++++++----- .../com/example/chaining/ui/theme/Theme.kt | 30 ++-- app/src/main/res/drawable/google.png | Bin 0 -> 1247 bytes 3 files changed, 114 insertions(+), 48 deletions(-) create mode 100644 app/src/main/res/drawable/google.png diff --git a/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt b/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt index 765e7be..edccd6f 100644 --- a/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt @@ -5,21 +5,24 @@ import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.example.chaining.BuildConfig +import com.example.chaining.R import com.google.android.gms.auth.api.identity.BeginSignInRequest import com.google.android.gms.auth.api.identity.Identity import com.google.firebase.Firebase @@ -30,26 +33,31 @@ import com.google.firebase.auth.auth fun LoginScreen(onLoginSuccess: () -> Unit) { val context = LocalContext.current val signInClient = Identity.getSignInClient(context) + var isLoading by remember { mutableStateOf(false) } val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartIntentSenderForResult() ) { result -> + isLoading = false if (result.resultCode == Activity.RESULT_OK) { val intent = result.data if (intent != null) { val credential = signInClient.getSignInCredentialFromIntent(intent) val idToken = credential.googleIdToken - if (idToken != null) { + isLoading = true val firebaseCredential = GoogleAuthProvider.getCredential(idToken, null) Firebase.auth.signInWithCredential(firebaseCredential) .addOnCompleteListener { task -> + isLoading = false if (task.isSuccessful) { onLoginSuccess() } else { Toast.makeText(context, "로그인 실패", Toast.LENGTH_SHORT).show() } } + } else { + Toast.makeText(context, "ID 토큰 없음", Toast.LENGTH_SHORT).show() } } else { Toast.makeText(context, "로그인 데이터 없음", Toast.LENGTH_SHORT).show() @@ -60,36 +68,86 @@ fun LoginScreen(onLoginSuccess: () -> Unit) { } Column( - modifier = Modifier.fillMaxSize().padding(16.dp), + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + .navigationBarsPadding() + .padding(horizontal = 20.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically) ) { - Text("Welcome to the App!", style = MaterialTheme.typography.headlineMedium) - Spacer(modifier = Modifier.height(32.dp)) + // 1) 체인 이미지 + Image( + painter = painterResource(id = R.drawable.chain), + contentDescription = "Chain", + modifier = Modifier + .size(90.dp) // 필요하면 조절 + .padding(bottom = 18.dp) + ) - Button(onClick = { - val signInRequest = BeginSignInRequest.builder() - .setGoogleIdTokenRequestOptions( - BeginSignInRequest.GoogleIdTokenRequestOptions.builder() - .setSupported(true) - .setServerClientId(BuildConfig.GOOGLE_API_WEB_CLIENT_ID) - .setFilterByAuthorizedAccounts(false) - .build() - ) - .setAutoSelectEnabled(true) - .build() + // 2) 체이닝 텍스트 + Text( + text = "Chaining", + fontSize = 50.sp, + fontWeight = FontWeight.ExtraBold, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 180.dp) + ) - signInClient.beginSignIn(signInRequest) - .addOnSuccessListener { result -> - val intentSenderRequest = IntentSenderRequest.Builder(result.pendingIntent).build() - launcher.launch(intentSenderRequest) - } - .addOnFailureListener { - Toast.makeText(context, "로그인 요청 실패", Toast.LENGTH_SHORT).show() + // 3) 구글 로그인 버튼 + Button( + onClick = { + isLoading = true + val signInRequest = BeginSignInRequest.builder() + .setGoogleIdTokenRequestOptions( + BeginSignInRequest.GoogleIdTokenRequestOptions.builder() + .setSupported(true) + .setServerClientId(BuildConfig.GOOGLE_API_WEB_CLIENT_ID) + .setFilterByAuthorizedAccounts(false) // 모든 계정 노출 + .build() + ) + .setAutoSelectEnabled(false) // 자동 선택 비활성화 → 계정 선택 보장 + .build() + + signInClient.beginSignIn(signInRequest) + .addOnSuccessListener { result -> + val intentSenderRequest = + IntentSenderRequest.Builder(result.pendingIntent).build() + launcher.launch(intentSenderRequest) + } + .addOnFailureListener { + isLoading = false + Toast.makeText(context, "로그인 요청 실패", Toast.LENGTH_SHORT).show() + } + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ), + enabled = !isLoading, + shape = RoundedCornerShape(12.dp), + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .shadow(4.dp, RoundedCornerShape(12.dp)) + .border(0.5.dp, Color.Gray, RoundedCornerShape(12.dp)) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + runCatching { + Image( + painter = painterResource(id = R.drawable.google), + contentDescription = "Google", + modifier = Modifier.size(24.dp) + ) } - }) { - Text("Google 계정으로 로그인") + Text( + text = if (isLoading) "로그인 중..." else "Google 계정으로 로그인", + style = MaterialTheme.typography.labelLarge.copy(fontSize = 18.sp) + ) + } } } -} - +} \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/ui/theme/Theme.kt b/app/src/main/java/com/example/chaining/ui/theme/Theme.kt index aac1cff..8dd4b24 100644 --- a/app/src/main/java/com/example/chaining/ui/theme/Theme.kt +++ b/app/src/main/java/com/example/chaining/ui/theme/Theme.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext private val DarkColorScheme = @@ -22,15 +23,22 @@ private val LightColorScheme = primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40, - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ + background = Color.White, // 앱 배경 흰색 + surface = Color.White, // Card, Surface 등도 흰색 + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color.Black, // 기본 텍스트 색상 + onSurface = Color.Black, // Surface 위 텍스트 색상 + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ ) @Composable @@ -52,8 +60,8 @@ fun chainingTheme( } MaterialTheme( - colorScheme = colorScheme, + colorScheme = LightColorScheme, typography = Typography, content = content, ) -} +} \ No newline at end of file diff --git a/app/src/main/res/drawable/google.png b/app/src/main/res/drawable/google.png new file mode 100644 index 0000000000000000000000000000000000000000..e7d106c4421e680481973b9d78dfbe4f0f09ed97 GIT binary patch literal 1247 zcmV<51R(o~P)&lwoS zv4M#T8#EAv6lREH%)$!hlrbof0Rsj>U?6cOHW}-UyE4eQ=@U=aNtZ<6ZtsnS#6bQxm2hqWk*7Q9* z4HS}z3*U{N2Niy>?(j+fwt{FE4}K8I;W+WD*Wk+tPBeIjfEs`AJ$NiK{D(w`;H|L? zPeLWGg(hhVP-qKKCMS0RDtjCf-k`Ha1OmFc>Wp`0(toU`oNpz+hCl01R4oIvJ3bUJOz3rK^rY# zCUsyY!O`9TfONw7mEVx?dL#j}Y<^k5-ccEV3jYV6Nm!r)y@WPmz$A}f1;12|1kg$U zIlG*c^rMX$Fv+w|1(rG#@BqpYi8gA$OkQy*V3KtRnJ-u~3t}`}J7GK8)OXjtfQg)Q zDZr5#z7Po3%)%RW*B1E*Jai)v>MMcWHMgvM1Rl8-K%0Ffu%mhso@#;ZRo{Z=AEBGF zFkcDm*50%|-HZ5>Q67ufOpdtOMmv2aa6~se&qF|V=1K4mxo4rCiT~rgUJ-~yPRnrq zrs$8pvA7Z_ztA}9UVsZ!ID?-Z4+a&jghN-ep~Dz^No;|Wo%Q|h?treZ;<$SOKp4!j zdrROpGYRxqIAj|RJK*ijqm`D6*I(Nv7JOTedS_&f$?6LJ^;)xq;wXMk^T0$l+TnjU zZHBWyC4p4(Ba(D9%k40^2XZH2w8I;zXksLVM+Tr#TN_~1t z(3j_Oy4jf4EFJB2S~&N60jE>V0oW6n;?% zk+miVe0u$tzn$pn5_E8UVmNNcb2Uujf;S|N>2pEVdSRX#t_%_deuaemEE7K~WKH~^ zo?{i$Mq}vu{pRj7Mm6UeGOeOtc|)ePmyKyg-pDY$C$xc$n|jBP1Uz~TaEihwM^pAYC3IE zWl&n}cgTJS%r#`%)P}Uhs%y#fE$n`_&lpo+AA|hy#~ Date: Sun, 17 Aug 2025 20:09:22 +0900 Subject: [PATCH 017/224] =?UTF-8?q?[Feat]=20=EC=98=A4=ED=94=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EC=BA=90=EC=8B=9C=20=EC=84=A4=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=AA=A8=EB=8D=B8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 9 +++++- app/google-services.json | 8 +++++ .../java/com/example/chaining/ChainingApp.kt | 9 +++++- .../data/repository/AreaRepository.kt | 2 ++ .../com/example/chaining/di/FirebaseModule.kt | 31 +++++++++++++++++++ .../chaining/domain/model/Application.kt | 12 +++++++ .../chaining/domain/model/RecruitPost.kt | 23 ++++++++++++++ .../com/example/chaining/domain/model/User.kt | 23 ++++++++++++++ 8 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/example/chaining/di/FirebaseModule.kt create mode 100644 app/src/main/java/com/example/chaining/domain/model/Application.kt create mode 100644 app/src/main/java/com/example/chaining/domain/model/RecruitPost.kt create mode 100644 app/src/main/java/com/example/chaining/domain/model/User.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a3dba0f..0bd6f8e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,6 +5,9 @@ plugins { alias(libs.plugins.jetbrains.kotlin.android) id("com.google.gms.google-services") + // @kotlinx.serialization.Serializable을 쓰기 위한 plugin 적용 + id("org.jetbrains.kotlin.plugin.serialization") version "2.2.10" + // Hilt 의존성 주입 (DI) 라이브러리 사용 id("com.google.dagger.hilt.android") id("kotlin-kapt") @@ -85,6 +88,7 @@ dependencies { implementation(platform("com.google.firebase:firebase-bom:33.16.0")) implementation("com.google.firebase:firebase-analytics") implementation("com.google.firebase:firebase-auth-ktx") + implementation("com.google.firebase:firebase-database-ktx") // Realtime Database // Hilt 의존성 주입 (DI) 라이브러리 사용 implementation("com.google.dagger:hilt-android:2.55") @@ -102,7 +106,10 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1") // Navigation 라이브러리 의존성 주입 - implementation ("androidx.navigation:navigation-compose:2.7.7") + implementation("androidx.navigation:navigation-compose:2.7.7") + + // @kotlinx.serialization.Serializable을 쓰기 위한 의존성 주입 + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) diff --git a/app/google-services.json b/app/google-services.json index 2cb4281..247bb27 100644 --- a/app/google-services.json +++ b/app/google-services.json @@ -13,6 +13,14 @@ } }, "oauth_client": [ + { + "client_id": "719736077401-q1onru1a7hb5due08ugcqtvi5eeadu5a.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.example.chaining", + "certificate_hash": "712070ac693fda20b1cf67db9dbd01dfa6ed0a26" + } + }, { "client_id": "719736077401-t6tu672tn4m7mt7arskef2gk2oj8u3cn.apps.googleusercontent.com", "client_type": 1, diff --git a/app/src/main/java/com/example/chaining/ChainingApp.kt b/app/src/main/java/com/example/chaining/ChainingApp.kt index 573e214..d0a56ee 100644 --- a/app/src/main/java/com/example/chaining/ChainingApp.kt +++ b/app/src/main/java/com/example/chaining/ChainingApp.kt @@ -1,7 +1,14 @@ package com.example.chaining import android.app.Application +import com.google.firebase.database.FirebaseDatabase import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class ChainingApp : Application() +class ChainingApp : Application() { + override fun onCreate() { + super.onCreate() + // DB 인스턴스가 생성되기 전에 반드시 호출 + FirebaseDatabase.getInstance().setPersistenceEnabled(true) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt b/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt index 8fd6c6d..bd13144 100644 --- a/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt +++ b/app/src/main/java/com/example/chaining/data/repository/AreaRepository.kt @@ -4,7 +4,9 @@ import com.example.chaining.BuildConfig import com.example.chaining.data.model.AreaCodeResponse import com.example.chaining.network.AreaService import javax.inject.Inject +import javax.inject.Singleton +@Singleton class AreaRepository @Inject constructor( private val api: AreaService ) { diff --git a/app/src/main/java/com/example/chaining/di/FirebaseModule.kt b/app/src/main/java/com/example/chaining/di/FirebaseModule.kt new file mode 100644 index 0000000..62c95d3 --- /dev/null +++ b/app/src/main/java/com/example/chaining/di/FirebaseModule.kt @@ -0,0 +1,31 @@ +package com.example.chaining.di + +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.database.DatabaseReference +import com.google.firebase.database.FirebaseDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import jakarta.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object FirebaseModule { + + @Provides + @Singleton + fun provideFirebaseAuth(): FirebaseAuth = FirebaseAuth.getInstance() + + @Provides + @Singleton + fun provideFirebaseDatabase(): FirebaseDatabase { + // 특정 URL을 쓰고 싶다면: + // return FirebaseDatabase.getInstance("https://.firebasedatabase.app") + return FirebaseDatabase.getInstance("https://chaining-88dbd-default-rtdb.firebaseio.com/") + } + + @Provides + @Singleton + fun provideRootRef(db: FirebaseDatabase): DatabaseReference = db.reference +} \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/domain/model/Application.kt b/app/src/main/java/com/example/chaining/domain/model/Application.kt new file mode 100644 index 0000000..dc3f608 --- /dev/null +++ b/app/src/main/java/com/example/chaining/domain/model/Application.kt @@ -0,0 +1,12 @@ +package com.example.chaining.domain.model + +@kotlinx.serialization.Serializable +data class Application( + val id: String = "", + val recruitPostId: String = "", // 어떤 모집글에 지원했는지 + val recruitPostTitle: String = "", // 모집글 제목 (캐싱) + val applicant: UserSummary = UserSummary(), // 지원자 간단 프로필 + val preferredLanguages: List = emptyList(), + val introduction: String = "", // 자기소개 + val createdAt: Long = 0L +) \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/domain/model/RecruitPost.kt b/app/src/main/java/com/example/chaining/domain/model/RecruitPost.kt new file mode 100644 index 0000000..bdd96ea --- /dev/null +++ b/app/src/main/java/com/example/chaining/domain/model/RecruitPost.kt @@ -0,0 +1,23 @@ +package com.example.chaining.domain.model + +@kotlinx.serialization.Serializable +data class RecruitPost( + val id: String = "", + val title: String = "", + val travelStyle: String = "", // 선호 여행지 스타일 + val travelDate: String = "", // 여행 일자 + val withCar: Boolean = false, // 자차 여부 + val deadline: String = "", // 모집 마감일 + val preferredLanguages: List = emptyList(), // 선호하는 언어 정보 + val content: String = "", // 모집글 내용 + val createdAt: Long = 0L, // 작성 시각 + val owner: UserSummary = UserSummary(), // 작성자 프로필 (간단 정보) + val applicants: List = emptyList() // 지원자 리스트 +) + +@kotlinx.serialization.Serializable +data class UserSummary( // 간단 버전 (닉네임/사진 정도만) + val id: String = "", + val nickname: String = "", + val profileImageUrl: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/domain/model/User.kt b/app/src/main/java/com/example/chaining/domain/model/User.kt new file mode 100644 index 0000000..89c7ad4 --- /dev/null +++ b/app/src/main/java/com/example/chaining/domain/model/User.kt @@ -0,0 +1,23 @@ +package com.example.chaining.domain.model + +@kotlinx.serialization.Serializable +data class User( + val id: String = "", // DB key (uid) + val nickname: String = "", + val profileImageUrl: String = "", + val country: String = "", // 출신국 + val residence: String = "", // 거주지 + val preferredDestinations: List = emptyList(), // 선호 여행지 + val preferredLanguages: List = emptyList(), // 선호 언어 + 수준 + val isPublic: Boolean = true, // 모집/지원 현황 공개 여부 + val kakaoOpenChatUrl: String = "", // 카톡 오픈채팅 링크 + val recruitPosts: List = emptyList(), // 내가 모집한 글 + val applications: List = emptyList(), // 내가 지원한 글 + val createdAt: Long = 0L // 서버 타임스탬프 +) + +@kotlinx.serialization.Serializable +data class LanguagePref( + val language: String = "", + val level: Int = 0 // 0 ~ 10 +) \ No newline at end of file From ea0e774ddb5a262c2ad3ed20e05c85568fd4aaf2 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Sun, 17 Aug 2025 20:36:36 +0900 Subject: [PATCH 018/224] =?UTF-8?q?[Feat]=20User=20=EA=B4=80=EB=A0=A8=20CR?= =?UTF-8?q?UD=20Repository=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/repository/UserRepository.kt | 82 +++++++++++++++++++ .../com/example/chaining/domain/model/User.kt | 16 ++-- 2 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/example/chaining/data/repository/UserRepository.kt diff --git a/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt b/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt new file mode 100644 index 0000000..da2d366 --- /dev/null +++ b/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt @@ -0,0 +1,82 @@ +package com.example.chaining.data.repository + + +import com.example.chaining.domain.model.User +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.database.DataSnapshot +import com.google.firebase.database.DatabaseError +import com.google.firebase.database.DatabaseReference +import com.google.firebase.database.ValueEventListener +import com.google.firebase.database.ktx.getValue +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.tasks.await +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class UserRepository @Inject constructor( + private val auth: FirebaseAuth, + private val rootRef: DatabaseReference +) { + private fun uidOrThrow(): String = + auth.currentUser?.uid ?: error("로그인이 필요합니다.") + + private fun usersRef(): DatabaseReference = rootRef.child("users") + + /** Create (신규 유저 추가) */ + suspend fun addUser(user: User): String { + val uid = uidOrThrow() + + val newUser = user.copy( + id = uid, + createdAt = System.currentTimeMillis() + ) + + usersRef().child(uid).setValue(newUser).await() + return uid + } + + /** Read (단건) */ + suspend fun getUser(id: String): User? { + val snap = usersRef().child(id).get().await() + return snap.getValue()?.copy(id = id) + } + + /** Read (내 계정 정보, 한 번만 가져오기) */ + suspend fun getMyUser(): User? { + val uid = uidOrThrow() + return getUser(uid) + } + + /** Read (실시간 구독 - 내 계정, 변경사항이 있을 때마다 계속 가져오기) */ + fun observeMyUser(): Flow = callbackFlow { + val uid = uidOrThrow() + val ref = usersRef().child(uid) + + val listener = object : ValueEventListener { + override fun onDataChange(snapshot: DataSnapshot) { + trySend(snapshot.getValue()?.copy(id = uid)).isSuccess + } + + override fun onCancelled(error: DatabaseError) { + close(error.toException()) + } + } + + ref.addValueEventListener(listener) + awaitClose { ref.removeEventListener(listener) } + } + + /** Update (부분 수정) */ + suspend fun updateUser(id: String, updates: Map) { + if (updates.isEmpty()) return + usersRef().child(id).updateChildren(updates).await() + } + + /** Delete */ + suspend fun deleteUser(id: String) { + usersRef().child(id).removeValue().await() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/domain/model/User.kt b/app/src/main/java/com/example/chaining/domain/model/User.kt index 89c7ad4..7ca9ecd 100644 --- a/app/src/main/java/com/example/chaining/domain/model/User.kt +++ b/app/src/main/java/com/example/chaining/domain/model/User.kt @@ -2,18 +2,18 @@ package com.example.chaining.domain.model @kotlinx.serialization.Serializable data class User( - val id: String = "", // DB key (uid) + val id: String = "", // DB key (uid) val nickname: String = "", val profileImageUrl: String = "", - val country: String = "", // 출신국 - val residence: String = "", // 거주지 + val country: String = "", // 출신국 + val residence: String = "", // 거주지 val preferredDestinations: List = emptyList(), // 선호 여행지 val preferredLanguages: List = emptyList(), // 선호 언어 + 수준 - val isPublic: Boolean = true, // 모집/지원 현황 공개 여부 - val kakaoOpenChatUrl: String = "", // 카톡 오픈채팅 링크 - val recruitPosts: List = emptyList(), // 내가 모집한 글 - val applications: List = emptyList(), // 내가 지원한 글 - val createdAt: Long = 0L // 서버 타임스탬프 + val isPublic: Boolean = true, // 모집/지원 현황 공개 여부 + val kakaoOpenChatUrl: String = "", // 카톡 오픈채팅 링크 + val recruitPosts: List = emptyList(), // 내가 모집한 글 (Post ID만 저장) + val applications: List = emptyList(), // 내가 지원한 글 (Application ID만 저장) + val createdAt: Long = 0L // 서버 타임스탬프 ) @kotlinx.serialization.Serializable From 994564f8eb69dfc4c01c3c0a220c1d0ab0491930 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Sun, 17 Aug 2025 20:38:03 +0900 Subject: [PATCH 019/224] =?UTF-8?q?[Feat]=20Soft=20Delete=20=ED=94=8C?= =?UTF-8?q?=EB=9E=98=EA=B7=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/chaining/data/repository/UserRepository.kt | 5 +++-- app/src/main/java/com/example/chaining/domain/model/User.kt | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt b/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt index da2d366..45f039a 100644 --- a/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt +++ b/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt @@ -75,8 +75,9 @@ class UserRepository @Inject constructor( usersRef().child(id).updateChildren(updates).await() } - /** Delete */ + /** Delete (Soft Delete) */ suspend fun deleteUser(id: String) { - usersRef().child(id).removeValue().await() + val updates = mapOf("isDeleted" to true) + usersRef().child(id).updateChildren(updates).await() } } \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/domain/model/User.kt b/app/src/main/java/com/example/chaining/domain/model/User.kt index 7ca9ecd..28f4ded 100644 --- a/app/src/main/java/com/example/chaining/domain/model/User.kt +++ b/app/src/main/java/com/example/chaining/domain/model/User.kt @@ -13,7 +13,8 @@ data class User( val kakaoOpenChatUrl: String = "", // 카톡 오픈채팅 링크 val recruitPosts: List = emptyList(), // 내가 모집한 글 (Post ID만 저장) val applications: List = emptyList(), // 내가 지원한 글 (Application ID만 저장) - val createdAt: Long = 0L // 서버 타임스탬프 + val createdAt: Long = 0L, // 서버 타임스탬프 + val isDeleted: Boolean = false // Soft Delete 플래그 추가 ) @kotlinx.serialization.Serializable From 71158696d285693db71c1f85db915849ee70a7d7 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Sun, 17 Aug 2025 20:45:29 +0900 Subject: [PATCH 020/224] =?UTF-8?q?[Feat]=20=EB=82=B4=20=EA=B3=84=EC=A0=95?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=EC=97=90=20=EB=8C=80=ED=95=9C=20Viewmodel?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/repository/UserRepository.kt | 2 +- .../chaining/viewmodel/UserViewModel.kt | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt diff --git a/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt b/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt index 45f039a..2c2228f 100644 --- a/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt +++ b/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt @@ -51,7 +51,7 @@ class UserRepository @Inject constructor( } /** Read (실시간 구독 - 내 계정, 변경사항이 있을 때마다 계속 가져오기) */ - fun observeMyUser(): Flow = callbackFlow { + fun observeMyUsers(): Flow = callbackFlow { val uid = uidOrThrow() val ref = usersRef().child(uid) diff --git a/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt b/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt new file mode 100644 index 0000000..b0490a3 --- /dev/null +++ b/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt @@ -0,0 +1,49 @@ +package com.example.chaining.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.chaining.data.repository.UserRepository +import com.example.chaining.domain.model.User +import dagger.hilt.android.lifecycle.HiltViewModel +import jakarta.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +@HiltViewModel +class UserViewModel @Inject constructor( + private val repo: UserRepository +) : ViewModel() { + + // 내 User 정보 (null일 수도 있음) + private val _user = MutableStateFlow(null) + val user: StateFlow = _user + + init { + // 앱 시작 시 내 계정 실시간 구독 + viewModelScope.launch { + repo.observeMyUsers().collect { newUser -> + _user.value = newUser + } + } + } + + /** Create - 최초 회원가입 시 User 등록 */ + fun addUser(user: User) = viewModelScope.launch { + repo.addUser(user) + } + + /** Update - 일부 필드 수정 */ + fun updateUser(updates: Map) = viewModelScope.launch { + _user.value?.id?.let { uid -> + repo.updateUser(uid, updates) + } + } + + /** Delete - Soft Delete */ + fun deleteUser() = viewModelScope.launch { + _user.value?.id?.let { uid -> + repo.deleteUser(uid) + } + } +} \ No newline at end of file From d3a2f8460686e85d73a6d5780faef076e3212d6d Mon Sep 17 00:00:00 2001 From: hi2242 Date: Sun, 17 Aug 2025 21:39:44 +0900 Subject: [PATCH 021/224] =?UTF-8?q?[Feat]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B8=B0=EB=B3=B8=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/ui/screen/MyPageScreen.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt diff --git a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt new file mode 100644 index 0000000..f62d06c --- /dev/null +++ b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt @@ -0,0 +1,23 @@ +package com.example.chaining.ui.screen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun MyPageScreen(uid: String) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + ProfileSection() + Spacer(modifier = Modifier.height(24.dp)) + BasicInfoSection() + Spacer(modifier = Modifier.height(24.dp)) + ActionButtons() + } +} \ No newline at end of file From 55f7a54499ce3081417af23140295f84e3d4af27 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 18 Aug 2025 18:17:39 +0900 Subject: [PATCH 022/224] =?UTF-8?q?[Design]=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EC=84=B9=EC=85=98=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 5 +- .../chaining/ui/screen/MyPageScreen.kt | 50 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a3dba0f..827cf59 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -102,7 +102,10 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1") // Navigation 라이브러리 의존성 주입 - implementation ("androidx.navigation:navigation-compose:2.7.7") + implementation("androidx.navigation:navigation-compose:2.7.7") + + // Coil (Jetpack Compose용) + implementation("io.coil-kt.coil3:coil-compose:3.1.0") implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) diff --git a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt index f62d06c..b6f6515 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt @@ -1,11 +1,30 @@ package com.example.chaining.ui.screen +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +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 +import androidx.compose.ui.unit.sp +import coil.compose.rememberAsyncImagePainter @Composable fun MyPageScreen(uid: String) { @@ -20,4 +39,35 @@ fun MyPageScreen(uid: String) { Spacer(modifier = Modifier.height(24.dp)) ActionButtons() } +} + +@Composable +fun ProfileSection() { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box { + Image( + painter = rememberAsyncImagePainter(model = "프로필 URL"), + contentDescription = null, + modifier = Modifier + .size(80.dp) + .clip(CircleShape) + ) + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "프로필 변경", + modifier = Modifier + .align(Alignment.BottomEnd) + .background(Color.White, CircleShape) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Text("안녕안녕안녕", fontWeight = FontWeight.Bold) + Icon(Icons.Default.Edit, contentDescription = "닉네임 수정") + } + + Spacer(modifier = Modifier.height(4.dp)) + Text("팔로워 203 · 팔로우 106", color = Color.Gray, fontSize = 12.sp) + } } \ No newline at end of file From 6368dbb17d571956c03ae6a7392eb2be26fef6f1 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 18 Aug 2025 20:52:58 +0900 Subject: [PATCH 023/224] =?UTF-8?q?[Design]=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=84=B9=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 2 +- .../chaining/ui/screen/MyPageScreen.kt | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 827cf59..00960e4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -105,7 +105,7 @@ dependencies { implementation("androidx.navigation:navigation-compose:2.7.7") // Coil (Jetpack Compose용) - implementation("io.coil-kt.coil3:coil-compose:3.1.0") + implementation("io.coil-kt:coil-compose:2.6.0") implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) diff --git a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt index b6f6515..bb90b1b 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt @@ -14,9 +14,18 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -70,4 +79,35 @@ fun ProfileSection() { Spacer(modifier = Modifier.height(4.dp)) Text("팔로워 203 · 팔로우 106", color = Color.Gray, fontSize = 12.sp) } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BasicInfoSection() { + Column { + Text("기본 정보", fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(8.dp)) + + val countries = listOf("한국", "미국", "일본") + var selectedCountry by remember { mutableStateOf(countries[0]) } + + ExposedDropdownMenuBox( + expanded = false, + onExpandedChange = {} + ) { + TextField( + value = selectedCountry, + onValueChange = {}, + label = { Text("출신 국가 선택") }, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(false) } + ) + DropdownMenu( + expanded = false, + onDismissRequest = {} + ) { + + } + } + } } \ No newline at end of file From 50004bb5176e3f3ac73877523016cd51fe7c305d Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 18 Aug 2025 21:35:54 +0900 Subject: [PATCH 024/224] =?UTF-8?q?[Design]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=B2=84=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/misc.xml | 1 + .../chaining/ui/screen/MyPageScreen.kt | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/.idea/misc.xml b/.idea/misc.xml index 8978d23..0ad17cb 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,3 +1,4 @@ + diff --git a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt index bb90b1b..17b5454 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column 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 @@ -14,6 +15,8 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox @@ -110,4 +113,33 @@ fun BasicInfoSection() { } } } +} + +@Composable +fun ActionButtons() { + Column { + Button( + onClick = { /* 모집 현황 */ }, + modifier = Modifier.fillMaxWidth() + ) { + Text("모집 현황") + } + + Spacer(modifier = Modifier.height(8.dp)) + Button( + onClick = { /* 지원 현황 */ }, + modifier = Modifier.fillMaxWidth() + ) { + Text("지원 현황") + } + + Spacer(modifier = Modifier.height(24.dp)) + Button( + onClick = {/* 프로필 저장 */ }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF3578E5)) + ) { + Text("프로필 저장", color = Color.White) + } + } } \ No newline at end of file From a33f45b340a362f4b67bcb112fff46866cee99be Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 18 Aug 2025 21:36:29 +0900 Subject: [PATCH 025/224] =?UTF-8?q?[Design]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20Route=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/example/chaining/ui/navigation/Screen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt b/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt index 44510c7..4ad1aef 100644 --- a/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt +++ b/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt @@ -5,4 +5,5 @@ sealed class Screen(val route: String) { object Login : Screen("login") object Home : Screen("home") object Area : Screen("area") + object MyPage : Screen("mypage") } \ No newline at end of file From efdceee6d2e49d41ed744592191c08bab44560ca Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 18 Aug 2025 21:44:05 +0900 Subject: [PATCH 026/224] =?UTF-8?q?[Design]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=99=95=EC=9D=B8=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EA=B7=B8=EB=9E=98=ED=94=84=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/chaining/ui/navigation/NavGraph.kt | 7 ++++++- .../java/com/example/chaining/ui/navigation/Screen.kt | 2 +- .../java/com/example/chaining/ui/screen/HomeScreen.kt | 9 ++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt index 1475400..7d1cbbe 100644 --- a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt +++ b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt @@ -8,6 +8,7 @@ import androidx.navigation.compose.composable import com.example.chaining.ui.login.LoginScreen import com.example.chaining.ui.screen.AreaScreen import com.example.chaining.ui.screen.HomeScreen +import com.example.chaining.ui.screen.MyPageScreen import com.example.chaining.ui.screen.SplashScreen @Composable @@ -29,11 +30,15 @@ fun NavGraph(navController: NavHostController, modifier: Modifier = Modifier) { } composable("home") { HomeScreen( - onTableClick = { navController.navigate("area") } + onTableClick = { navController.navigate("area") }, + onMyPageClick = { navController.navigate("myPage") } ) } composable("area") { AreaScreen() } + composable("myPage") { + MyPageScreen(uid = "1") + } } } diff --git a/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt b/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt index 4ad1aef..2abcce7 100644 --- a/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt +++ b/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt @@ -5,5 +5,5 @@ sealed class Screen(val route: String) { object Login : Screen("login") object Home : Screen("home") object Area : Screen("area") - object MyPage : Screen("mypage") + object MyPage : Screen("myPage") } \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt index ebf92ec..10808a3 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt @@ -16,7 +16,7 @@ import com.google.firebase.Firebase import com.google.firebase.auth.auth @Composable -fun HomeScreen(onTableClick: () -> Unit) { +fun HomeScreen(onTableClick: () -> Unit, onMyPageClick: () -> Unit) { val user = Firebase.auth.currentUser Column( modifier = Modifier @@ -26,10 +26,17 @@ fun HomeScreen(onTableClick: () -> Unit) { verticalArrangement = Arrangement.Center ) { Text("환영합니다, ${user?.displayName}") + Spacer(modifier = Modifier.height(16.dp)) Button(onClick = onTableClick) { Text("테이블 보기") } + + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = onMyPageClick) { + Text("마이페이지") + } + Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { Firebase.auth.signOut() }) { Text("로그아웃") From dab63f0fdec39683027976d96b053a9613f2d554 Mon Sep 17 00:00:00 2001 From: lhs2257 Date: Mon, 18 Aug 2025 22:11:56 +0900 Subject: [PATCH 027/224] =?UTF-8?q?[Design]=20=EA=B5=AC=EA=B8=80=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=B2=84=ED=8A=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/google-services.json | 8 ++++++++ .../java/com/example/chaining/ui/login/LoginScreen.kt | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/google-services.json b/app/google-services.json index 2cb4281..247bb27 100644 --- a/app/google-services.json +++ b/app/google-services.json @@ -13,6 +13,14 @@ } }, "oauth_client": [ + { + "client_id": "719736077401-q1onru1a7hb5due08ugcqtvi5eeadu5a.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.example.chaining", + "certificate_hash": "712070ac693fda20b1cf67db9dbd01dfa6ed0a26" + } + }, { "client_id": "719736077401-t6tu672tn4m7mt7arskef2gk2oj8u3cn.apps.googleusercontent.com", "client_type": 1, diff --git a/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt b/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt index edccd6f..e53b482 100644 --- a/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt @@ -122,7 +122,9 @@ fun LoginScreen(onLoginSuccess: () -> Unit) { }, colors = ButtonDefaults.buttonColors( containerColor = Color.White, - contentColor = Color.Black + contentColor = Color.Black, + disabledContainerColor = Color.White, + disabledContentColor = Color.Black ), enabled = !isLoading, shape = RoundedCornerShape(12.dp), From b32afd76d9db227c391bbb75a4f16d96904dfaad Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 18 Aug 2025 22:28:25 +0900 Subject: [PATCH 028/224] =?UTF-8?q?[Design]=20Coil=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4101c5b..cd74f03 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -104,6 +104,9 @@ dependencies { // Navigation 라이브러리 의존성 주입 implementation("androidx.navigation:navigation-compose:2.7.7") + // Coil (Jetpack Compose용) + implementation("io.coil-kt:coil-compose:2.6.0") + // @kotlinx.serialization.Serializable을 쓰기 위한 의존성 주입 implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") From 386e70a4acee53e129bcc04a0f747c86d8cd34ff Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 18 Aug 2025 22:29:43 +0900 Subject: [PATCH 029/224] =?UTF-8?q?[Refactor]=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=AA=A8=EB=8D=B8=20=ED=95=AD=EB=AA=A9=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/example/chaining/domain/model/RecruitPost.kt | 1 + app/src/main/java/com/example/chaining/domain/model/User.kt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/example/chaining/domain/model/RecruitPost.kt b/app/src/main/java/com/example/chaining/domain/model/RecruitPost.kt index bdd96ea..6b6c325 100644 --- a/app/src/main/java/com/example/chaining/domain/model/RecruitPost.kt +++ b/app/src/main/java/com/example/chaining/domain/model/RecruitPost.kt @@ -11,6 +11,7 @@ data class RecruitPost( val preferredLanguages: List = emptyList(), // 선호하는 언어 정보 val content: String = "", // 모집글 내용 val createdAt: Long = 0L, // 작성 시각 + val kakaoOpenChatUrl: String = "", // 카톡 오픈채팅 링크 val owner: UserSummary = UserSummary(), // 작성자 프로필 (간단 정보) val applicants: List = emptyList() // 지원자 리스트 ) diff --git a/app/src/main/java/com/example/chaining/domain/model/User.kt b/app/src/main/java/com/example/chaining/domain/model/User.kt index 28f4ded..e42b879 100644 --- a/app/src/main/java/com/example/chaining/domain/model/User.kt +++ b/app/src/main/java/com/example/chaining/domain/model/User.kt @@ -10,7 +10,6 @@ data class User( val preferredDestinations: List = emptyList(), // 선호 여행지 val preferredLanguages: List = emptyList(), // 선호 언어 + 수준 val isPublic: Boolean = true, // 모집/지원 현황 공개 여부 - val kakaoOpenChatUrl: String = "", // 카톡 오픈채팅 링크 val recruitPosts: List = emptyList(), // 내가 모집한 글 (Post ID만 저장) val applications: List = emptyList(), // 내가 지원한 글 (Application ID만 저장) val createdAt: Long = 0L, // 서버 타임스탬프 From b94c9e7405fd31870c63f434249e713ea3c51792 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 18 Aug 2025 22:39:33 +0900 Subject: [PATCH 030/224] =?UTF-8?q?[Fix]=20Firebase=20Realtime=20Database?= =?UTF-8?q?=20Kotlin=20=ED=99=95=EC=9E=A5=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cd74f03..f541df3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -83,6 +83,7 @@ dependencies { // 구글 Firebase 사용 implementation(platform("com.google.firebase:firebase-bom:33.16.0")) + implementation("com.google.firebase:firebase-database-ktx") implementation("com.google.firebase:firebase-analytics") implementation("com.google.firebase:firebase-auth-ktx") From b1106e22ba327e232704e25113bf59a25dd4c6fe Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 18 Aug 2025 22:51:09 +0900 Subject: [PATCH 031/224] =?UTF-8?q?[Feat]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=8C=8C=EC=9D=B4=EC=96=B4=EB=B2=A0?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20DB=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/ui/screen/MyPageScreen.kt | 89 ++++++++++++++----- 1 file changed, 69 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt index 17b5454..4068602 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -36,29 +37,54 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.rememberAsyncImagePainter +import com.example.chaining.domain.model.User +import com.example.chaining.viewmodel.UserViewModel @Composable -fun MyPageScreen(uid: String) { +fun MyPageScreen( + userViewModel: UserViewModel = hiltViewModel() +) { + val userState by userViewModel.user.collectAsState() + Column( modifier = Modifier .fillMaxSize() .padding(16.dp) ) { - ProfileSection() + ProfileSection(user = userState, onNicknameChange = { newNickname -> + userViewModel.updateUser(mapOf("nickname" to newNickname)) + }) + Spacer(modifier = Modifier.height(24.dp)) - BasicInfoSection() + BasicInfoSection( + selectedCountry = userState?.country ?: "한국", + onCountryChange = { newCountry -> + userViewModel.updateUser(mapOf("country" to newCountry)) + } + ) + Spacer(modifier = Modifier.height(24.dp)) - ActionButtons() + ActionButtons( + onSave = { + // 필요하면 전체 유저 정보 저장 + } + ) } } @Composable -fun ProfileSection() { +fun ProfileSection( + user: User?, + onNicknameChange: (String) -> Unit +) { + var nickname by remember { mutableStateOf(user?.nickname ?: "") } + Column(horizontalAlignment = Alignment.CenterHorizontally) { Box { Image( - painter = rememberAsyncImagePainter(model = "프로필 URL"), + painter = rememberAsyncImagePainter(model = user?.profileImageUrl ?: ""), contentDescription = null, modifier = Modifier .size(80.dp) @@ -75,7 +101,15 @@ fun ProfileSection() { Spacer(modifier = Modifier.height(8.dp)) Row(verticalAlignment = Alignment.CenterVertically) { - Text("안녕안녕안녕", fontWeight = FontWeight.Bold) + TextField( + value = nickname, + onValueChange = { + nickname = it + onNicknameChange(it) + }, + singleLine = true, + modifier = Modifier.weight(1f) + ) Icon(Icons.Default.Edit, contentDescription = "닉네임 수정") } @@ -86,37 +120,52 @@ fun ProfileSection() { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun BasicInfoSection() { +fun BasicInfoSection( + selectedCountry: String, + onCountryChange: (String) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + val countries = listOf("한국", "미국", "일본") + var country by remember { mutableStateOf(selectedCountry) } + Column { Text("기본 정보", fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(8.dp)) - val countries = listOf("한국", "미국", "일본") - var selectedCountry by remember { mutableStateOf(countries[0]) } - ExposedDropdownMenuBox( - expanded = false, - onExpandedChange = {} + expanded = expanded, + onExpandedChange = { expanded = !expanded } ) { TextField( - value = selectedCountry, - onValueChange = {}, + value = country, + onValueChange = { }, label = { Text("출신 국가 선택") }, readOnly = true, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(false) } + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) } ) DropdownMenu( - expanded = false, - onDismissRequest = {} + expanded = expanded, + onDismissRequest = { expanded = false } ) { - + countries.forEach { c -> + androidx.compose.material3.DropdownMenuItem( + text = { Text(c) }, + onClick = { + country = c + onCountryChange(c) + expanded = false + } + ) + } } } } } @Composable -fun ActionButtons() { +fun ActionButtons( + onSave: () -> Unit +) { Column { Button( onClick = { /* 모집 현황 */ }, From e95b83a30dd915676f558de06642565d980244b8 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Mon, 18 Aug 2025 23:03:16 +0900 Subject: [PATCH 032/224] =?UTF-8?q?[Fix]=20=EC=98=AC=EB=B0=94=EB=A5=B8=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EC=9D=98=20Inject=EB=A5=BC=20import=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/example/chaining/viewmodel/UserViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt b/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt index b0490a3..dfd4571 100644 --- a/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt +++ b/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt @@ -5,10 +5,10 @@ import androidx.lifecycle.viewModelScope import com.example.chaining.data.repository.UserRepository import com.example.chaining.domain.model.User import dagger.hilt.android.lifecycle.HiltViewModel -import jakarta.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class UserViewModel @Inject constructor( From 63840ebcb3e8921dd8d4be6ecc1cf0cbac1f1390 Mon Sep 17 00:00:00 2001 From: lhs2257 Date: Mon, 18 Aug 2025 23:39:36 +0900 Subject: [PATCH 033/224] =?UTF-8?q?[Design]=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=99=88=20=ED=99=95=EC=9D=B8=20=EB=B2=84=ED=8A=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EA=B7=B8=EB=9E=98=ED=94=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 ++++ .../java/com/example/chaining/di/FirebaseModule.kt | 2 +- .../com/example/chaining/ui/navigation/NavGraph.kt | 3 ++- .../java/com/example/chaining/ui/navigation/Screen.kt | 1 + .../java/com/example/chaining/ui/screen/HomeScreen.kt | 11 ++++++++++- .../com/example/chaining/viewmodel/UserViewModel.kt | 2 +- gradle/libs.versions.toml | 2 ++ 7 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4101c5b..f541df3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -83,6 +83,7 @@ dependencies { // 구글 Firebase 사용 implementation(platform("com.google.firebase:firebase-bom:33.16.0")) + implementation("com.google.firebase:firebase-database-ktx") implementation("com.google.firebase:firebase-analytics") implementation("com.google.firebase:firebase-auth-ktx") @@ -104,6 +105,9 @@ dependencies { // Navigation 라이브러리 의존성 주입 implementation("androidx.navigation:navigation-compose:2.7.7") + // Coil (Jetpack Compose용) + implementation("io.coil-kt:coil-compose:2.6.0") + // @kotlinx.serialization.Serializable을 쓰기 위한 의존성 주입 implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") diff --git a/app/src/main/java/com/example/chaining/di/FirebaseModule.kt b/app/src/main/java/com/example/chaining/di/FirebaseModule.kt index 62c95d3..48a13db 100644 --- a/app/src/main/java/com/example/chaining/di/FirebaseModule.kt +++ b/app/src/main/java/com/example/chaining/di/FirebaseModule.kt @@ -7,7 +7,7 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import jakarta.inject.Singleton +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt index 7d1cbbe..649abd9 100644 --- a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt +++ b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt @@ -31,7 +31,8 @@ fun NavGraph(navController: NavHostController, modifier: Modifier = Modifier) { composable("home") { HomeScreen( onTableClick = { navController.navigate("area") }, - onMyPageClick = { navController.navigate("myPage") } + onMyPageClick = { navController.navigate("myPage") }, + onMainHomeClick = { navController.navigate("mainHome") } ) } composable("area") { diff --git a/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt b/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt index 2abcce7..ffdf0b8 100644 --- a/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt +++ b/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt @@ -6,4 +6,5 @@ sealed class Screen(val route: String) { object Home : Screen("home") object Area : Screen("area") object MyPage : Screen("myPage") + object MainHome : Screen("mainHome") } \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt index 10808a3..9ef0772 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt @@ -16,7 +16,11 @@ import com.google.firebase.Firebase import com.google.firebase.auth.auth @Composable -fun HomeScreen(onTableClick: () -> Unit, onMyPageClick: () -> Unit) { +fun HomeScreen( + onTableClick: () -> Unit, + onMyPageClick: () -> Unit, + onMainHomeClick: () -> Unit +) { val user = Firebase.auth.currentUser Column( modifier = Modifier @@ -27,6 +31,11 @@ fun HomeScreen(onTableClick: () -> Unit, onMyPageClick: () -> Unit) { ) { Text("환영합니다, ${user?.displayName}") + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = onMainHomeClick) { + Text("메인 홈") + } + Spacer(modifier = Modifier.height(16.dp)) Button(onClick = onTableClick) { Text("테이블 보기") diff --git a/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt b/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt index b0490a3..2843f89 100644 --- a/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt +++ b/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.viewModelScope import com.example.chaining.data.repository.UserRepository import com.example.chaining.domain.model.User import dagger.hilt.android.lifecycle.HiltViewModel -import jakarta.inject.Inject +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4560751..9a815c0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ espressoCore = "3.6.1" lifecycleRuntimeKtx = "2.8.6" activityCompose = "1.9.3" composeBom = "2024.04.01" +firebaseDatabaseKtx = "22.0.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -24,6 +25,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +firebase-database-ktx = { group = "com.google.firebase", name = "firebase-database-ktx", version.ref = "firebaseDatabaseKtx" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 98fc78d5c6d61446b8d84af09c6d2cdc3f146e34 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Tue, 19 Aug 2025 00:20:31 +0900 Subject: [PATCH 034/224] =?UTF-8?q?[Feat]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20Firebase=20Realtime=20Database=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/data/repository/UserRepository.kt | 6 ++++++ .../java/com/example/chaining/di/FirebaseModule.kt | 2 +- .../com/example/chaining/ui/navigation/NavGraph.kt | 3 ++- .../com/example/chaining/ui/screen/HomeScreen.kt | 1 + .../com/example/chaining/ui/screen/MyPageScreen.kt | 9 ++++----- .../example/chaining/viewmodel/UserViewModel.kt | 14 ++++++++++++++ 6 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt b/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt index 2c2228f..924d0d2 100644 --- a/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt +++ b/app/src/main/java/com/example/chaining/data/repository/UserRepository.kt @@ -75,6 +75,12 @@ class UserRepository @Inject constructor( usersRef().child(id).updateChildren(updates).await() } + /** 전체 User 객체 저장 */ + suspend fun saveUser(user: User) { + val uid = uidOrThrow() + usersRef().child(uid).setValue(user).await() + } + /** Delete (Soft Delete) */ suspend fun deleteUser(id: String) { val updates = mapOf("isDeleted" to true) diff --git a/app/src/main/java/com/example/chaining/di/FirebaseModule.kt b/app/src/main/java/com/example/chaining/di/FirebaseModule.kt index 62c95d3..48a13db 100644 --- a/app/src/main/java/com/example/chaining/di/FirebaseModule.kt +++ b/app/src/main/java/com/example/chaining/di/FirebaseModule.kt @@ -7,7 +7,7 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import jakarta.inject.Singleton +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt index 7d1cbbe..0684592 100644 --- a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt +++ b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt @@ -13,6 +13,7 @@ import com.example.chaining.ui.screen.SplashScreen @Composable fun NavGraph(navController: NavHostController, modifier: Modifier = Modifier) { + NavHost( navController = navController, startDestination = "splash", @@ -38,7 +39,7 @@ fun NavGraph(navController: NavHostController, modifier: Modifier = Modifier) { AreaScreen() } composable("myPage") { - MyPageScreen(uid = "1") + MyPageScreen() } } } diff --git a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt index 10808a3..3b3901d 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt @@ -18,6 +18,7 @@ import com.google.firebase.auth.auth @Composable fun HomeScreen(onTableClick: () -> Unit, onMyPageClick: () -> Unit) { val user = Firebase.auth.currentUser + Column( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt index 4068602..9829fc1 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt @@ -67,9 +67,7 @@ fun MyPageScreen( Spacer(modifier = Modifier.height(24.dp)) ActionButtons( - onSave = { - // 필요하면 전체 유저 정보 저장 - } + onSave = { userState?.let { userViewModel.saveUser() } } ) } } @@ -141,7 +139,8 @@ fun BasicInfoSection( onValueChange = { }, label = { Text("출신 국가 선택") }, readOnly = true, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) } + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, + modifier = Modifier.menuAnchor() ) DropdownMenu( expanded = expanded, @@ -184,7 +183,7 @@ fun ActionButtons( Spacer(modifier = Modifier.height(24.dp)) Button( - onClick = {/* 프로필 저장 */ }, + onClick = onSave, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF3578E5)) ) { diff --git a/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt b/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt index dfd4571..693a271 100644 --- a/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt +++ b/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt @@ -1,5 +1,6 @@ package com.example.chaining.viewmodel +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.chaining.data.repository.UserRepository @@ -33,10 +34,23 @@ class UserViewModel @Inject constructor( repo.addUser(user) } + /** Update - 전체 User 객체 저장 */ + fun saveUser() = viewModelScope.launch { + try { + _user.value?.let { + repo.saveUser(it) + Log.d("UserVM", "User saved: $it") + } ?: Log.w("UserVM", "_user.value is null, skipping save") + } catch (e: Exception) { + Log.e("UserVM", "Failed to save user", e) + } + } + /** Update - 일부 필드 수정 */ fun updateUser(updates: Map) = viewModelScope.launch { _user.value?.id?.let { uid -> repo.updateUser(uid, updates) + Log.d("UserVM", "$uid, $updates") } } From 53afebcc64e5570ac068b142bb5a7b7aebb00bf1 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Tue, 19 Aug 2025 01:26:12 +0900 Subject: [PATCH 035/224] =?UTF-8?q?[Feat]=20=EC=8B=A0=EA=B7=9C=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20Firebase=20Realtime=20Database=EC=9D=98?= =?UTF-8?q?=20users=EC=97=90=20=EC=B6=94=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EA=B3=84=EC=A0=95=20=EC=8B=A4?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EA=B5=AC=EB=8F=85=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/chaining/ui/login/LoginScreen.kt | 30 ++++++++++++++++--- .../chaining/ui/navigation/NavGraph.kt | 10 ++++--- .../chaining/viewmodel/UserViewModel.kt | 12 ++++++-- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt b/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt index 765e7be..aaedd10 100644 --- a/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/login/LoginScreen.kt @@ -19,7 +19,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.example.chaining.BuildConfig +import com.example.chaining.domain.model.User +import com.example.chaining.viewmodel.UserViewModel import com.google.android.gms.auth.api.identity.BeginSignInRequest import com.google.android.gms.auth.api.identity.Identity import com.google.firebase.Firebase @@ -27,7 +30,10 @@ import com.google.firebase.auth.GoogleAuthProvider import com.google.firebase.auth.auth @Composable -fun LoginScreen(onLoginSuccess: () -> Unit) { +fun LoginScreen( + onLoginSuccess: () -> Unit, + userViewModel: UserViewModel = hiltViewModel() +) { val context = LocalContext.current val signInClient = Identity.getSignInClient(context) @@ -45,7 +51,20 @@ fun LoginScreen(onLoginSuccess: () -> Unit) { Firebase.auth.signInWithCredential(firebaseCredential) .addOnCompleteListener { task -> if (task.isSuccessful) { - onLoginSuccess() + val firebaseUser = Firebase.auth.currentUser + if (firebaseUser != null) { + val uid = firebaseUser.uid + + // 신규 유저 DB 등록 + userViewModel.addUser( + User( + id = uid, + nickname = firebaseUser.displayName ?: "" + ) + ) + // 로그인 성공 처리 + onLoginSuccess() + } } else { Toast.makeText(context, "로그인 실패", Toast.LENGTH_SHORT).show() } @@ -60,7 +79,9 @@ fun LoginScreen(onLoginSuccess: () -> Unit) { } Column( - modifier = Modifier.fillMaxSize().padding(16.dp), + modifier = Modifier + .fillMaxSize() + .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { @@ -81,7 +102,8 @@ fun LoginScreen(onLoginSuccess: () -> Unit) { signInClient.beginSignIn(signInRequest) .addOnSuccessListener { result -> - val intentSenderRequest = IntentSenderRequest.Builder(result.pendingIntent).build() + val intentSenderRequest = + IntentSenderRequest.Builder(result.pendingIntent).build() launcher.launch(intentSenderRequest) } .addOnFailureListener { diff --git a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt index 0684592..fee1655 100644 --- a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt +++ b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt @@ -23,11 +23,13 @@ fun NavGraph(navController: NavHostController, modifier: Modifier = Modifier) { SplashScreen(navController) } composable("login") { - LoginScreen { - navController.navigate("home") { - popUpTo("login") { inclusive = true } + LoginScreen( + onLoginSuccess = { + navController.navigate("home") { + popUpTo("login") { inclusive = true } + } } - } + ) } composable("home") { HomeScreen( diff --git a/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt b/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt index 693a271..972a4d8 100644 --- a/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt +++ b/app/src/main/java/com/example/chaining/viewmodel/UserViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.chaining.data.repository.UserRepository import com.example.chaining.domain.model.User +import com.google.firebase.auth.ktx.auth +import com.google.firebase.ktx.Firebase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -22,9 +24,13 @@ class UserViewModel @Inject constructor( init { // 앱 시작 시 내 계정 실시간 구독 - viewModelScope.launch { - repo.observeMyUsers().collect { newUser -> - _user.value = newUser + val currentUser = Firebase.auth.currentUser + if (currentUser != null) { + // 사용자가 로그인한 경우에만 데이터를 구독 + viewModelScope.launch { + repo.observeMyUsers().collect { newUser -> + _user.value = newUser + } } } } From b6e077fe1a90bf0d82286027dc07b5030c08135f Mon Sep 17 00:00:00 2001 From: lhs2257 Date: Tue, 19 Aug 2025 03:36:38 +0900 Subject: [PATCH 036/224] =?UTF-8?q?[Design]=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=99=88=20UI=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/ui/navigation/NavGraph.kt | 4 + .../chaining/ui/screen/MainHomeScreen.kt | 391 ++++++++++++++++++ app/src/main/res/drawable/alarm.png | Bin 0 -> 2030 bytes app/src/main/res/drawable/clock.png | Bin 0 -> 2747 bytes app/src/main/res/drawable/hamburger.png | Bin 0 -> 557 bytes app/src/main/res/drawable/home.png | Bin 0 -> 1519 bytes app/src/main/res/drawable/people.png | Bin 0 -> 2689 bytes app/src/main/res/drawable/search.png | Bin 0 -> 1792 bytes app/src/main/res/drawable/selected_home.png | Bin 0 -> 307 bytes 9 files changed, 395 insertions(+) create mode 100644 app/src/main/java/com/example/chaining/ui/screen/MainHomeScreen.kt create mode 100644 app/src/main/res/drawable/alarm.png create mode 100644 app/src/main/res/drawable/clock.png create mode 100644 app/src/main/res/drawable/hamburger.png create mode 100644 app/src/main/res/drawable/home.png create mode 100644 app/src/main/res/drawable/people.png create mode 100644 app/src/main/res/drawable/search.png create mode 100644 app/src/main/res/drawable/selected_home.png diff --git a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt index 649abd9..89ab719 100644 --- a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt +++ b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt @@ -8,6 +8,7 @@ import androidx.navigation.compose.composable import com.example.chaining.ui.login.LoginScreen import com.example.chaining.ui.screen.AreaScreen import com.example.chaining.ui.screen.HomeScreen +import com.example.chaining.ui.screen.MainHomeScreen import com.example.chaining.ui.screen.MyPageScreen import com.example.chaining.ui.screen.SplashScreen @@ -41,5 +42,8 @@ fun NavGraph(navController: NavHostController, modifier: Modifier = Modifier) { composable("myPage") { MyPageScreen(uid = "1") } + composable("mainHome"){ + MainHomeScreen() + } } } diff --git a/app/src/main/java/com/example/chaining/ui/screen/MainHomeScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/MainHomeScreen.kt new file mode 100644 index 0000000..69ee445 --- /dev/null +++ b/app/src/main/java/com/example/chaining/ui/screen/MainHomeScreen.kt @@ -0,0 +1,391 @@ +package com.example.chaining.ui.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.chaining.R +import androidx.compose.foundation.layout.* +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.IconButton +import androidx.compose.material3.Surface +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage + +// OptIn annotation for using experimental Material 3 APIs +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainHomeScreen() { + Scaffold( + topBar = { + // TODO: 1. 상단 앱 바 구현 + TopAppBar(title = { Text("Chaining") }, + // actions: 앱 바 오른쪽에 위치하는 요소들 + navigationIcon = { + IconButton(onClick = { /* TODO: 메뉴 열기 기능 */ }) { + Icon( + painter = painterResource(id = R.drawable.hamburger), + contentDescription = "메뉴" + ) + } + }, + actions = { + // 방금 만든 프로필 이미지 컴포저블 호출 + ProfileImageWithStatus( + // 임시 이미지 URL 사용 + model = "https://newsimg-hams.hankookilbo.com/2023/03/24/4531dada-e9cf-4775-951c-902e3558ca41.jpg", + isOnline = true, + modifier = Modifier.padding(end = 16.dp) // 오른쪽에 여백 추가 + ) + } + ) + }, + bottomBar = { + // TODO: 2. 하단 네비게이션 바 구현 + AppBottomNavigation() + }, + ) { innerPadding -> + // TODO: 3. 중앙 콘텐츠 구현 (환영 메시지, 매칭 카드, 팔로우 목록 등) + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 16.dp) + ) { + Text( + "오팔만님 반갑습니다.", // TODO: 실제 사용자 닉네임으로 변경 + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(top = 16.dp) + .padding(start = 8.dp) + ) + Text( + text = "최근 접수된 지원서", + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + color = Color.Gray, + modifier = Modifier + .padding(top = 24.dp) + .padding(start = 8.dp) + ) + MatchingRequestCard() + + Text( + text = "최근 팔로우 목록", + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + color = Color.Gray, + modifier = Modifier + .padding(top = 24.dp) + .padding(start = 8.dp) + ) + FollowerListItem( + name = "강호동", + timestamp = "23분 전", + imageUrl = "https://spnimage.edaily.co.kr/images/photo/files/NP/S/2015/03/PS15032500193.jpg" + ) + FollowerListItem( + name = "차무식", + timestamp = "1시간 전", + imageUrl = "https://newsimg-hams.hankookilbo.com/2023/03/24/4531dada-e9cf-4775-951c-902e3558ca41.jpg" + ) + } + } +} + +@Composable +fun AppBottomNavigation() { + Surface( + modifier = Modifier + .fillMaxWidth() + .height(75.dp) + .background(color = Color.White) + .border(width = 1.dp, color = Color.LightGray), + shadowElevation = 30.dp + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + CustomIconButton( + onClick = { /*TODO*/ }, + iconRes = R.drawable.selected_home, // 아이콘 변경 필요 + description = "메인 홈" + ) + CustomIconButton( + onClick = { /*TODO*/ }, + iconRes = R.drawable.people, + description = "매칭" + ) + CustomIconButton( + onClick = { /*TODO*/ }, + iconRes = R.drawable.search, + description = "검색" + ) + CustomIconButton( + onClick = { /*TODO*/ }, + iconRes = R.drawable.alarm, + description = "알림" + ) + } + } +} + +@Composable +private fun CustomIconButton( + onClick: () -> Unit, + iconRes: Int, + description: String +) { + Box( + modifier = Modifier + .clickable( + onClick = onClick, + // 리플 효과를 없애기 위한 핵심 코드 + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) + // 버튼의 터치 영역을 적절히 확보하기 위한 패딩 + .padding(10.dp), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = description, + modifier = Modifier.size(30.dp) + ) + } +} + +@Composable +fun ProfileImageWithStatus( + model : Any, + isOnline: Boolean, + modifier: Modifier = Modifier +) { + // Box를 사용해 이미지와 상태 점을 겹치게 만듭니다. + Box(modifier = modifier.size(40.dp)) { + // 프로필 이미지 + AsyncImage( + model = model, + contentDescription = "프로필 사진", + contentScale = ContentScale.Crop, + modifier = Modifier + .matchParentSize() // 부모(Box) 크기에 맞춤 + .clip(RoundedCornerShape(15.dp)) // 이미지를 원형으로 자름 + ) + + // 온라인 상태를 표시하는 점 + if (isOnline) { + Box( + modifier = Modifier + .size(12.dp) + .align(Alignment.BottomEnd) // 오른쪽 아래에 배치 + .background(Color(0xFF00C853), CircleShape) // 초록색 배경 + .border(width = 1.5.dp, color = Color.White, shape = CircleShape) // 흰색 테두리 + ) + } + } +} + +@Composable +fun MatchingRequestCard() { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), // 소제목과의 간격 + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF4285F4) // 파란색 배경 + ) + ) { + Column( + // 카드 내부 콘텐츠들을 위한 여백 + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "제주도 하이킹 함께 하실 분!", + color = Color.White, + fontSize = 12.sp, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically // 아이콘과 텍스트를 세로 중앙 정렬 + ) { + Icon( + painter = painterResource(id = R.drawable.clock), + contentDescription = "남은 시간", + tint = Color.White, + modifier = Modifier.size(20.dp) // 아이콘 크기 조절 + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Text( + text = "수락/거절까지 12시간 30분 남음", + color = Color.White, + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors( + containerColor = Color.White + ) + ) { + Column( + modifier = Modifier.padding(12.dp) + ){ + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 프로필 사진 + AsyncImage( + model = "https://newsimg-hams.hankookilbo.com/2023/03/24/4531dada-e9cf-4775-951c-902e3558ca41.jpg", + contentDescription = "신청자 프로필 사진", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(15.dp)) + ) + + // 사진과 이름 사이 간격 + Spacer(modifier = Modifier.width(12.dp)) + + // 이름과 태그 + Column( + modifier = Modifier.weight(1f) // 남는 공간을 모두 차지 + ) { + Text( + text = "차무식", + fontWeight = FontWeight.Bold, + color = Color.Black + ) + Text( + text = "필리핀", + fontSize = 12.sp, + color = Color.Gray + ) + } + + // 지원서 보기 텍스트 + Text( + text = "지원서 보기 >", + fontSize = 12.sp, + color = Color.Gray + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 수락 버튼 + Button( + onClick = { /*TODO*/ }, + modifier = Modifier.weight(2f), // 남은 공간의 절반 차지 + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF4285F4), // 더 진한 파란색 + contentColor = Color.White + ) + ) { + Text("수락") + } + + // 거절 버튼 + Button( + onClick = { /*TODO*/ }, + modifier = Modifier.weight(1f), // 남은 공간의 절반 차지 + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFFEBEFFA), + contentColor = Color.Gray + ) + ) { + Text("거절") + } + } + } + + } + } + } +} + +@Composable +fun FollowerListItem(name: String, timestamp: String, imageUrl: String) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Color.White + ) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + ProfileImageWithStatus(model = imageUrl, isOnline = true) + + Spacer(modifier = Modifier.width(12.dp)) + + Column { + Text( + text = "${name}님께서 팔로우를 하셨습니다.", + fontWeight = FontWeight.SemiBold, + color = Color.Black + ) + Text( + text = timestamp, + fontSize = 12.sp, + color = Color.Gray + ) + } + } + } +} + +@Composable +fun MainHomeScreenPreview() { + MainHomeScreen() +} \ No newline at end of file diff --git a/app/src/main/res/drawable/alarm.png b/app/src/main/res/drawable/alarm.png new file mode 100644 index 0000000000000000000000000000000000000000..d88ef6698a450354a712289a0370c0a78bcea4cd GIT binary patch literal 2030 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 BL-GIs literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/hamburger.png b/app/src/main/res/drawable/hamburger.png new file mode 100644 index 0000000000000000000000000000000000000000..bafbb02406d66350d591698879d138665ef40a4b GIT binary patch literal 557 zcmeAS@N?(olHy`uVBq!ia0vp^DImmAu8wslJ0f~hYK6#5Ch%l-A|!I|Ro#h)2D^8mo86Eu;N&kuk-YM<4c98 zRcuE&*708~J$9+0I^ov&uE3wOcV8^^;_3a{Zln8p@9n+KM`Uf)+thYivG||=#a?(< zy+e&N=-RI9)+dT~?vaUYce!{Y{rLAq8hWCZuXgPf^@(pdhB*^1WN6>utY!?zcq4Bj3KgUVZQuuUzHj1;_v8@y~wtcB-Qx%rOkl)=A3<#KtdP zI(h2;+|R}Pbmo>gOYE{e+jX(`8ppJ{HGkTFyt$TmEu(jnAOE!n56(Z^v8nC6!935` zrt>m4UzBB=ky*<&!ZEbszuBFc@A7zpt#`T<>Qw`*^k04 e1Tg~UFxN9YpY6DNi~Rf;kbtMFpUXO@geCxiboP}1 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/home.png b/app/src/main/res/drawable/home.png new file mode 100644 index 0000000000000000000000000000000000000000..e26327d13a16543268cab09021653a18621ce084 GIT binary patch literal 1519 zcmVLeh~ej*lMw$qRU_$u|4nkY9$ zQ2`)zmbx%a>KoclO?$MXb15ClOG1VIplL{#hHWlHlSp!s3Y>n=(5 zZNV8q5F84cHz>_hO7ma;7C_##ve7tPx;jH@wZ=(+{PMv<%n{`^74WGdN)}>ZbO9jk zjuYb`QNF5Rc#(*bB7k%gApP1(jM+lJgQnX>6vtF5!$&GjkJ<|{ceKzGK`^pJgmEDT z4xbK4Z?r4Me+Bv7zQLh0MU)Nz46js3ds~hG&F`4fS`^xAo++Xvr^onyg*;&qyq~Pq z8b{B#wZ|+z|0f`gnL0!m3wi@S7Bp{dH(=Wae$f2fRYBu;5v2)LJ#;o`{=jBRTl1rf zW1r*GN}H*%LjJfC44o^YECBdX4K)2|A%@kW&nzAl(km7tWgqCUE~3oRQ$@N9kR~m} zNVF}|+dF#L%=SvFS=f~MoY{^kwJ=u)Yfl8E7cE@NZ%Xsc?bcJp=sJb8-(tNoz8b@m zy)vQ(`FueB)MBdo2@FOnI<2p!*PR2JKe8AtUsRAT^vXm9`3BJZi-j1^SF3b_vwADT z$APA2?M5W6^@>hXpFTP|dPv?eD+q!kydp9%5S<3dJ1j=U&!EXq+aB;k0L?#G*zhX=qhb!*1^^pC3*sT|vd?OKn^$XnHS8*V=KgRLI{i)?hi% zWs{FiD9m)Sw>zfi?PJa(6!NyTw?MMS*g9i#t9ETOec*Ki=1c)JziAN+cbkk6ub^2j zO7kwW6OgO1#_a&|s}?hMc@yKU!+qU?Ix(TwSq3zQ07<+C5e=68XMdtW$Mb zH8+j(|5HQWSH%duf>Ff?P1~Bd=E+$6_ma@F9)Qrn@ovc&tm}< zw`lsNF9?@;1*b0vmnqHP#)?v>Rhb!MG6CZSDbM+I6)9-qy0&EbH-PWrW>a3tfVeDt zq`jFe7^@hb7e7q-8n0x4rfcK+Ba;-P>4brWr+X7+OlYqCvr93N+YeMBkHq-Q&%BZX zF)R5@{t5xe_bx05@9;_r#Ow~6zRI9-8jBer<6cPtr16+O8Ox$_5o8V`NS7gkbQvN@ zmmz|586rrRA%b)nB1o4Zf^-=oNS9GML4sgpiP>caMrp$bHv)LjyxyEVP zS1Li2&x1Av;iGnpIZYpZA@%0@}|7(H3{Ng+L+fzm^*88X<$9NqODi&(Gg;f~G7-ESdC=c~+bJf! zUm-tee6`D>G(QBI?sS=hYo2AEIsI(M_JXDdjOQ?Y;>un@5ClOG1VIo4K@hy0`5Vr! Vz4BYseaZj;002ovPDHLkV1kK(x@Q0Y literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/people.png b/app/src/main/res/drawable/people.png new file mode 100644 index 0000000000000000000000000000000000000000..035187befcaac8214b02a29fa04aca96b8660ecd GIT binary patch literal 2689 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/search.png b/app/src/main/res/drawable/search.png new file mode 100644 index 0000000000000000000000000000000000000000..d7f3a9ee83b24db156ac1d223dd6e6fac1e7e1a7 GIT binary patch literal 1792 zcmV+b2mknqP)S#|NR95Yn^vxr2AD z+reIBP~Ev}-5Ep0QV9nPsn2Rr*oO>*B6?7LG0T)82r2?i(=4G)szg=SDKl zKKIOc=g!&p?jM|wGjq=QpS9Ood#|+yAP@)y0)apv5C{YUfj}S-2#lI2>O0NMtX^-3 zi!pK8qRfqvAJ@yG^w<*LAD1e(M@ip#;Wdu|#bWhbLp;l%Ja1998RVmuP4$4zKA-Vo;f3r|1 zPqN5;YC)NN3-O);R&Rn15#zna&X)hM#LJqU-1YbOPc&31Wf8`Z{8lJdr$W1;OH3Ba zFeLAGC%u=Cb)I8M7St^;%~F1flAdd!9ckz@P!3sSY2`iHB2O_Sn;U#2-`PYigtm*O z8ibuE>L*4^p-?{4qP*GQ{@&N+4%z{YSkgjUgYvB@Rxo7a2@!B>+3Qk2Yov0wj`?^R(^E)OuiI+U~nVPxx2a&JR-)* zbt%i%W8ecZR_}H`M5%fUc!nYV%&}~(2Op?ahYasKwu6Tl;(3neIjo^Q zAMz388SPwhc9*9lmi5fNrx#_c}U$QqLQ-kyaVi?$v+AHf)08~6UC6ct|dg{b$@Yf@{k;K-yiSy zxoYM(bXQrYBA!=(q}EGc4o&(73pRLD>dzv7+3HZel}s2MQj`7?Y_l%p0|xo2w7sdUmeSol=K^SB zRG6Y#%t(EJ&0-Nc>;`3V*5xLMatvD4q4mNB`9tmLI{_hf)`9@6OdHKfUl7GLI|VD# zw2jOnKdV``hQpTl>-r9WzVxkH-f2-@)@VzkuV$6%1W|HGl)=&Ko?SaT`rHu(QSwI= zM9Cj2O2Yw~Vfkt;>ga``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{s~VP$B+ufx6?NA9x@Pc-QUP4aZpt`psFS3 z0GFobX2zTY99Mi8r45+!RzA>?Y+Uo>4fAo{x=X=JBF>$kuGhsVDO;%~|8Lhp7M+ue z`8jPTNVOU~F%|l9X>Iur#_MVGIXLhBtX1N7QUHZG+$A&mi1rj>fsOS7rYZFNxF9Tp6TXyT)Fd^!j~sL4mlML^b>=ptDnm{r-UW|`dV Date: Tue, 19 Aug 2025 22:14:34 +0900 Subject: [PATCH 037/224] =?UTF-8?q?[Design]=20=EB=93=9C=EB=A1=AD=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/chaining/MainActivity.kt | 8 +- .../com/example/chaining/domain/model/User.kt | 2 +- .../chaining/ui/screen/MyPageScreen.kt | 181 ++++++++++++++---- .../com/example/chaining/ui/theme/Theme.kt | 16 +- app/src/main/res/drawable/airport.png | Bin 0 -> 4454 bytes app/src/main/res/drawable/change.png | Bin 0 -> 7376 bytes app/src/main/res/drawable/country.png | Bin 0 -> 5787 bytes app/src/main/res/drawable/forest_path.png | Bin 0 -> 4190 bytes app/src/main/res/drawable/pen_squared.png | Bin 0 -> 1488 bytes app/src/main/res/drawable/triangle_arrow.png | Bin 0 -> 3117 bytes .../main/res/drawable/voice_recognition.png | Bin 0 -> 6008 bytes 11 files changed, 157 insertions(+), 50 deletions(-) create mode 100644 app/src/main/res/drawable/airport.png create mode 100644 app/src/main/res/drawable/change.png create mode 100644 app/src/main/res/drawable/country.png create mode 100644 app/src/main/res/drawable/forest_path.png create mode 100644 app/src/main/res/drawable/pen_squared.png create mode 100644 app/src/main/res/drawable/triangle_arrow.png create mode 100644 app/src/main/res/drawable/voice_recognition.png diff --git a/app/src/main/java/com/example/chaining/MainActivity.kt b/app/src/main/java/com/example/chaining/MainActivity.kt index 09d20ec..7a2543c 100644 --- a/app/src/main/java/com/example/chaining/MainActivity.kt +++ b/app/src/main/java/com/example/chaining/MainActivity.kt @@ -4,8 +4,10 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier import androidx.navigation.compose.rememberNavController @@ -24,7 +26,11 @@ class MainActivity : ComponentActivity() { chainingTheme { val navController = rememberNavController() - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Scaffold( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { innerPadding -> NavGraph( navController = navController, modifier = Modifier.padding(innerPadding) diff --git a/app/src/main/java/com/example/chaining/domain/model/User.kt b/app/src/main/java/com/example/chaining/domain/model/User.kt index e42b879..0d03668 100644 --- a/app/src/main/java/com/example/chaining/domain/model/User.kt +++ b/app/src/main/java/com/example/chaining/domain/model/User.kt @@ -7,7 +7,7 @@ data class User( val profileImageUrl: String = "", val country: String = "", // 출신국 val residence: String = "", // 거주지 - val preferredDestinations: List = emptyList(), // 선호 여행지 + val preferredDestinations: String = "", // 선호 여행지 val preferredLanguages: List = emptyList(), // 선호 언어 + 수준 val isPublic: Boolean = true, // 모집/지원 현황 공개 여부 val recruitPosts: List = emptyList(), // 내가 모집한 글 (Post ID만 저장) diff --git a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt index 9829fc1..1a999bd 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt @@ -1,7 +1,11 @@ package com.example.chaining.ui.screen +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -11,19 +15,23 @@ 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.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -33,12 +41,15 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.rememberAsyncImagePainter +import com.example.chaining.R import com.example.chaining.domain.model.User import com.example.chaining.viewmodel.UserViewModel @@ -56,13 +67,34 @@ fun MyPageScreen( ProfileSection(user = userState, onNicknameChange = { newNickname -> userViewModel.updateUser(mapOf("nickname" to newNickname)) }) - - Spacer(modifier = Modifier.height(24.dp)) - BasicInfoSection( - selectedCountry = userState?.country ?: "한국", - onCountryChange = { newCountry -> - userViewModel.updateUser(mapOf("country" to newCountry)) - } + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = "기본 정보", + fontWeight = FontWeight.SemiBold, + ) + Spacer(modifier = Modifier.height(12.dp)) + DropDownField( + items = listOf("한국", "미국", "일본"), + selectedItem = userState?.country ?: "한국", + onItemSelected = { country -> userViewModel.updateUser(mapOf("country" to country)) }, + leadingIconRes = R.drawable.airport, + placeholder = "출신 국가 선택" + ) + Spacer(modifier = Modifier.height(12.dp)) + DropDownField( + items = listOf("서울", "부산", "제주"), + selectedItem = userState?.residence ?: "서울", + onItemSelected = { residence -> userViewModel.updateUser(mapOf("residence" to residence)) }, + leadingIconRes = R.drawable.country, + placeholder = "현재 거주지 선택" + ) + Spacer(modifier = Modifier.height(12.dp)) + DropDownField( + items = listOf("파리", "도쿄", "뉴욕"), + selectedItem = userState?.preferredDestinations ?: "파리", + onItemSelected = { preferredDestinations -> userViewModel.updateUser(mapOf("preferredDestinations" to preferredDestinations)) }, + leadingIconRes = R.drawable.forest_path, + placeholder = "선호 여행지 선택" ) Spacer(modifier = Modifier.height(24.dp)) @@ -118,45 +150,118 @@ fun ProfileSection( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun BasicInfoSection( - selectedCountry: String, - onCountryChange: (String) -> Unit +fun DropDownField( + items: List, // 드롭다운 항목 + selectedItem: String, // 선택된 값 + onItemSelected: (String) -> Unit, // 선택 시 콜백 + leadingIconRes: Int, // 아이콘 리소스 + placeholder: String, // Placeholder 텍스트 + modifier: Modifier = Modifier ) { var expanded by remember { mutableStateOf(false) } - val countries = listOf("한국", "미국", "일본") - var country by remember { mutableStateOf(selectedCountry) } + val rotation by animateFloatAsState( + targetValue = if (expanded) 180f else 0f, + animationSpec = tween(durationMillis = 300) + ) - Column { - Text("기본 정보", fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) + Column(modifier = Modifier.fillMaxWidth()) { ExposedDropdownMenuBox( expanded = expanded, - onExpandedChange = { expanded = !expanded } + onExpandedChange = { expanded = !expanded }, + modifier = Modifier + .fillMaxWidth() + .background(Color.White) + .border( + width = 1.dp, + color = Color(0xFF637387), + shape = RoundedCornerShape(12.dp) + ) ) { - TextField( - value = country, - onValueChange = { }, - label = { Text("출신 국가 선택") }, - readOnly = true, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, - modifier = Modifier.menuAnchor() - ) - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - countries.forEach { c -> - androidx.compose.material3.DropdownMenuItem( - text = { Text(c) }, - onClick = { - country = c - onCountryChange(c) - expanded = false - } - ) + Crossfade( + targetState = selectedItem, + animationSpec = tween(300), + label = "countryCrossfade" + ) { animatedItem -> + TextField( + value = animatedItem, + onValueChange = { }, + readOnly = true, + textStyle = LocalTextStyle.current.copy( + fontWeight = FontWeight.SemiBold, + color = Color(0xFF637387) + ), + label = { + Text( + placeholder, + fontSize = 10.sp, + color = Color(0xD9637387), + fontWeight = FontWeight.Medium, + ) + }, + leadingIcon = { + Icon( + painter = painterResource(id = leadingIconRes), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .height(24.dp) + .width(24.dp) + ) + }, + trailingIcon = { + Icon( + painter = painterResource(id = R.drawable.triangle_arrow), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .height(20.dp) + .width(20.dp) + .rotate(rotation) + ) + }, + colors = TextFieldDefaults.colors( + focusedTextColor = Color(0xFF637387), + unfocusedTextColor = Color(0xFF637387), + disabledTextColor = Color(0xFF637387), + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White, + disabledContainerColor = Color.White, + cursorColor = Color(0xFF637387), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + modifier = Modifier + .menuAnchor() + .fillMaxWidth() + ) + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier + .exposedDropdownSize() + .background(Color.White) + ) { + items.forEach { c -> + DropdownMenuItem( + text = { + Text( + text = c, + color = Color(0xFF637387), + fontWeight = if (c == selectedItem) FontWeight.Bold else FontWeight.Normal + ) + }, + onClick = { + onItemSelected(c) + expanded = false + } + ) + } } } + } } } diff --git a/app/src/main/java/com/example/chaining/ui/theme/Theme.kt b/app/src/main/java/com/example/chaining/ui/theme/Theme.kt index aac1cff..e09e8f6 100644 --- a/app/src/main/java/com/example/chaining/ui/theme/Theme.kt +++ b/app/src/main/java/com/example/chaining/ui/theme/Theme.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext private val DarkColorScheme = @@ -22,22 +23,17 @@ private val LightColorScheme = primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40, - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ + background = Color.White, // 원하는 색상 (#FFFFBF) + surface = Color.White, // Scaffold, Surface 등 표면도 같은 색상 + onBackground = Color.Black, + onSurface = Color.Black ) @Composable fun chainingTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, + dynamicColor: Boolean = false, content: @Composable () -> Unit, ) { val colorScheme = diff --git a/app/src/main/res/drawable/airport.png b/app/src/main/res/drawable/airport.png new file mode 100644 index 0000000000000000000000000000000000000000..3ba5705a43a45b40ccf346e9d34ef82d409ce7bf GIT binary patch literal 4454 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/change.png b/app/src/main/res/drawable/change.png new file mode 100644 index 0000000000000000000000000000000000000000..88db22a89d298cd0fadf024a952948f0d47fcb9a GIT binary patch literal 7376 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/pen_squared.png b/app/src/main/res/drawable/pen_squared.png new file mode 100644 index 0000000000000000000000000000000000000000..af0d1b76e0a695f9a966a2f475cce62b464922c7 GIT binary patch literal 1488 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 Hu0mjf<;&$> literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/voice_recognition.png b/app/src/main/res/drawable/voice_recognition.png new file mode 100644 index 0000000000000000000000000000000000000000..8b50df6bdff60fffd9a2c2f3a67bb263ce74d506 GIT binary patch literal 6008 zcmV-;7l-JHP){0^^?KgEW8Ji=gZ00w>=ZIO>*;+Jl?+fybjkoS=|G zJRn(|rw2XtYq-V>!WWpH3A+5q=%4PI3qyz6AWoz-Bp%>`Ul{t{J(RJTK)~|V4QAm$ z#tj^Mdt+_QYx|(FvB&1?Ok@awuQZpynFliFzmA;8xwZi~cNuRp3vfDV&}FwAIjZhH z1l83HdVszrm_k8;lGAQciWcve{hnZst_S-`YRIW8#4AazR_?X7>|B-;0+≶zIbH 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 mA%zrDNFjw3Qb^%!!v6t}Ozu65!zKd&0000 Date: Tue, 19 Aug 2025 23:25:49 +0900 Subject: [PATCH 038/224] =?UTF-8?q?[Feat]=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=AA=A8=EB=8B=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/ui/screen/MyPageScreen.kt | 117 +++++++++++++----- 1 file changed, 86 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt index 1a999bd..a4eadda 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt @@ -6,6 +6,8 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -19,7 +21,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -109,45 +110,99 @@ fun ProfileSection( user: User?, onNicknameChange: (String) -> Unit ) { - var nickname by remember { mutableStateOf(user?.nickname ?: "") } + var showDialog by remember { mutableStateOf(false) } + var nicknameInput by remember { mutableStateOf(user?.nickname ?: "") } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box { + Image( + painter = rememberAsyncImagePainter(model = user?.profileImageUrl ?: ""), + contentDescription = null, + modifier = Modifier + .size(80.dp) + .clip(CircleShape) + ) + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "프로필 변경", + modifier = Modifier + .align(Alignment.BottomEnd) + .background(Color.White, CircleShape) + ) + } - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Box { - Image( - painter = rememberAsyncImagePainter(model = user?.profileImageUrl ?: ""), - contentDescription = null, - modifier = Modifier - .size(80.dp) - .clip(CircleShape) - ) - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = "프로필 변경", + Spacer(modifier = Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, modifier = Modifier - .align(Alignment.BottomEnd) - .background(Color.White, CircleShape) - ) - } + .clickable { + nicknameInput = user?.nickname ?: "" + showDialog = true + } + ) { + Text( + text = user?.nickname ?: "닉네임 없음", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + ) + Spacer(modifier = Modifier.width(4.dp)) + Icon( + painter = painterResource(id = R.drawable.pen_squared), + contentDescription = "닉네임 수정", + tint = Color.Unspecified, + modifier = Modifier.size(20.dp) + ) + } - Spacer(modifier = Modifier.height(8.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - TextField( - value = nickname, - onValueChange = { - nickname = it - onNicknameChange(it) - }, - singleLine = true, - modifier = Modifier.weight(1f) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "팔로워 203 · 팔로우 106", + color = Color.Gray, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold ) - Icon(Icons.Default.Edit, contentDescription = "닉네임 수정") } + } + + - Spacer(modifier = Modifier.height(4.dp)) - Text("팔로워 203 · 팔로우 106", color = Color.Gray, fontSize = 12.sp) + if (showDialog) { + androidx.compose.material3.AlertDialog( + onDismissRequest = { showDialog = false }, + title = { Text("닉네임 변경") }, + text = { + TextField( + value = nicknameInput, + onValueChange = { nicknameInput = it }, + placeholder = { Text("닉네임 입력") }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + }, + confirmButton = { + Button( + onClick = { + onNicknameChange(nicknameInput) + showDialog = false + } + ) { + Text("변경") + } + }, + dismissButton = { + Button(onClick = { showDialog = false }) { + Text("취소") + } + } + ) } } + @OptIn(ExperimentalMaterial3Api::class) @Composable fun DropDownField( From df47b135573d362c6f58b645730eab4edde800bd Mon Sep 17 00:00:00 2001 From: lhs2257 Date: Wed, 20 Aug 2025 15:30:33 +0900 Subject: [PATCH 039/224] =?UTF-8?q?[Design]=20=EB=AA=A8=EC=A7=91=EA=B8=80?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=20=ED=99=95=EC=9D=B8=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/example/chaining/ui/navigation/NavGraph.kt | 2 +- .../main/java/com/example/chaining/ui/screen/HomeScreen.kt | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt index 89ab719..3f8730b 100644 --- a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt +++ b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt @@ -33,7 +33,7 @@ fun NavGraph(navController: NavHostController, modifier: Modifier = Modifier) { HomeScreen( onTableClick = { navController.navigate("area") }, onMyPageClick = { navController.navigate("myPage") }, - onMainHomeClick = { navController.navigate("mainHome") } + onMainHomeClick = { navController.navigate("mainHome") }, ) } composable("area") { diff --git a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt index 9ef0772..4588ae8 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt @@ -50,5 +50,10 @@ fun HomeScreen( Button(onClick = { Firebase.auth.signOut() }) { Text("로그아웃") } + + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = onCreatePostClick) { + Text(text = "모집글 작성") + } } } \ No newline at end of file From d9ea963f50e22d185a05f97bb860b5027c946baf Mon Sep 17 00:00:00 2001 From: lhs2257 Date: Wed, 20 Aug 2025 15:36:58 +0900 Subject: [PATCH 040/224] =?UTF-8?q?[Design]=20=EB=AA=A8=EC=A7=91=EA=B8=80?= =?UTF-8?q?=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B7=B8?= =?UTF-8?q?=EB=9E=98=ED=94=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/example/chaining/ui/navigation/NavGraph.kt | 4 ++++ .../main/java/com/example/chaining/ui/navigation/Screen.kt | 1 + 2 files changed, 5 insertions(+) diff --git a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt index 3f8730b..10b7bdc 100644 --- a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt +++ b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt @@ -34,6 +34,7 @@ fun NavGraph(navController: NavHostController, modifier: Modifier = Modifier) { onTableClick = { navController.navigate("area") }, onMyPageClick = { navController.navigate("myPage") }, onMainHomeClick = { navController.navigate("mainHome") }, + onCreatPostClick = {navController.navigate("createPost") } ) } composable("area") { @@ -45,5 +46,8 @@ fun NavGraph(navController: NavHostController, modifier: Modifier = Modifier) { composable("mainHome"){ MainHomeScreen() } + composable("createPost") { + CreatePostScreen() + } } } diff --git a/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt b/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt index ffdf0b8..b462bcf 100644 --- a/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt +++ b/app/src/main/java/com/example/chaining/ui/navigation/Screen.kt @@ -7,4 +7,5 @@ sealed class Screen(val route: String) { object Area : Screen("area") object MyPage : Screen("myPage") object MainHome : Screen("mainHome") + object CreatePost : Screen("createPost") } \ No newline at end of file From eb3f2eb5a896eb40a6e69f05d209fcc92b275019 Mon Sep 17 00:00:00 2001 From: lhs2257 Date: Wed, 20 Aug 2025 17:22:35 +0900 Subject: [PATCH 041/224] =?UTF-8?q?[Feat]=20=EB=AA=A8=EC=A7=91=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/ui/navigation/NavGraph.kt | 3 +- .../chaining/ui/screen/CreatePostScreen.kt | 229 ++++++++++++++++++ .../example/chaining/ui/screen/HomeScreen.kt | 3 +- .../chaining/ui/screen/MainHomeScreen.kt | 1 - app/src/main/res/drawable/back_arrow.png | Bin 0 -> 521 bytes app/src/main/res/drawable/down_arrow.png | Bin 0 -> 1209 bytes app/src/main/res/drawable/favorite_spot.png | Bin 0 -> 1695 bytes 7 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/example/chaining/ui/screen/CreatePostScreen.kt create mode 100644 app/src/main/res/drawable/back_arrow.png create mode 100644 app/src/main/res/drawable/down_arrow.png create mode 100644 app/src/main/res/drawable/favorite_spot.png diff --git a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt index 10b7bdc..614b982 100644 --- a/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt +++ b/app/src/main/java/com/example/chaining/ui/navigation/NavGraph.kt @@ -7,6 +7,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import com.example.chaining.ui.login.LoginScreen import com.example.chaining.ui.screen.AreaScreen +import com.example.chaining.ui.screen.CreatePostScreen import com.example.chaining.ui.screen.HomeScreen import com.example.chaining.ui.screen.MainHomeScreen import com.example.chaining.ui.screen.MyPageScreen @@ -34,7 +35,7 @@ fun NavGraph(navController: NavHostController, modifier: Modifier = Modifier) { onTableClick = { navController.navigate("area") }, onMyPageClick = { navController.navigate("myPage") }, onMainHomeClick = { navController.navigate("mainHome") }, - onCreatPostClick = {navController.navigate("createPost") } + onCreatePostClick = { navController.navigate("createPost") } ) } composable("area") { diff --git a/app/src/main/java/com/example/chaining/ui/screen/CreatePostScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/CreatePostScreen.kt new file mode 100644 index 0000000..a5ac871 --- /dev/null +++ b/app/src/main/java/com/example/chaining/ui/screen/CreatePostScreen.kt @@ -0,0 +1,229 @@ +package com.example.chaining.ui.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +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.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.chaining.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreatePostScreen() { + var title by remember { mutableStateOf("") } // 제목을 저장할 상태 변수 + var content by remember { mutableStateOf("") } // 내용을 위한 상태 변수 + + Scaffold( + topBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.dp) // 원하는 높이로 직접 설정 + .clip(RoundedCornerShape(bottomEnd = 20.dp)) + .background(Color(0xFF4A526A)), + // 내부 요소들을 세로 중앙에 정렬 + verticalAlignment = Alignment.CenterVertically + ) { + // 뒤로가기 아이콘 버튼 + IconButton(onClick = { /* TODO: 뒤로 가기 */ }) { + Icon( + painter = painterResource(id = R.drawable.back_arrow), + contentDescription = "뒤로 가기", + modifier = Modifier.size(20.dp), + tint = Color.White + ) + } + + // 제목 텍스트 + Text( + text = "모집글 작성", + modifier = Modifier.weight(1f), // 3. 남는 공간을 모두 차지 + color = Color.White, + fontSize = 20.sp, + textAlign = TextAlign.Center // 4. 텍스트를 가운데 정렬 + ) + + // 제목을 완벽한 중앙에 맞추기 위한 빈 공간 + Spacer(modifier = Modifier.width(48.dp)) + } + }, + // 전체 화면 배경색 설정 + containerColor = Color(0xFFF3F6FF) + ) { innerPadding -> + // 스크롤 가능한 Column으로 콘텐츠 영역 설정 + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(16.dp) // 좌우, 상하 여백 + .verticalScroll(rememberScrollState()) // 스크롤 가능하게 만듦 + ) { + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedTextField( + value = title, + onValueChange = { title = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("제목을 입력하세요. (50자 이내)") }, + shape = RoundedCornerShape(16.dp), + singleLine = true, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White, + disabledContainerColor = Color.White, + focusedPlaceholderColor = Color.Gray, + unfocusedPlaceholderColor = Color.Gray, + focusedIndicatorColor = Color.LightGray, + unfocusedIndicatorColor = Color.LightGray + ) + ) + Spacer(modifier = Modifier.height(20.dp)) + + // '선호하는 여행지 선택' 드롭다운 메뉴 추가 + PreferenceSelector() + Spacer(modifier = Modifier.height(16.dp)) + PreferenceSelector() + Spacer(modifier = Modifier.height(16.dp)) + PreferenceSelector() + Spacer(modifier = Modifier.height(16.dp)) + PreferenceSelector() + + // 내용 입력창 + Spacer(modifier = Modifier.height(16.dp)) + OutlinedTextField( + value = content, + onValueChange = { content = it }, + modifier = Modifier + .fillMaxWidth() + .height(200.dp), // 높이를 200dp로 지정 + placeholder = { Text("내용을 입력하세요. (500자 이내)") }, + shape = RoundedCornerShape(16.dp), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White, + focusedPlaceholderColor = Color.Gray, + unfocusedPlaceholderColor = Color.Gray, + focusedIndicatorColor = Color.LightGray, + unfocusedIndicatorColor = Color.LightGray + ) + ) + + Spacer(modifier = Modifier.height(36.dp)) + + Button( + onClick = { /* TODO: 작성 완료 로직 구현 */ }, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + //.padding(bottom = 16.dp), + shape = RoundedCornerShape(30.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF4285F4) // 파란색 배경 + ) + ) { + Text(text = "작성 완료", fontSize = 16.sp) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PreferenceSelector() { + // 드롭다운 메뉴에 표시할 아이템 목록 + val options = listOf("서울", "부산", "제주도", "강릉", "경주") + // 드롭다운 메뉴가 펼쳐졌는지 여부를 저장하는 상태 + var isExpanded by remember { mutableStateOf(false) } + // 현재 선택된 항목을 저장하는 상태 + var selectedOptionText by remember { mutableStateOf("") } + + ExposedDropdownMenuBox( + expanded = isExpanded, + onExpandedChange = { isExpanded = !isExpanded } + ) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .menuAnchor(), + readOnly = true, + value = selectedOptionText, + onValueChange = {}, + placeholder = { Text("선호하는 여행지 선택") }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.favorite_spot), + contentDescription = null, + tint = Color(0xFF4285F4) + ) + }, + trailingIcon = { + Icon( + painter = painterResource(id = R.drawable.down_arrow), + contentDescription = "드롭다운 메뉴 열기", + modifier = Modifier.size(16.dp), + tint = Color.LightGray + + ) + }, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White, + focusedPlaceholderColor = Color.Gray, + unfocusedPlaceholderColor = Color.LightGray, + focusedIndicatorColor = Color.LightGray, + unfocusedIndicatorColor = Color.LightGray + ), + shape = RoundedCornerShape(16.dp) + ) + ExposedDropdownMenu( + expanded = isExpanded, + onDismissRequest = { isExpanded = false }, + modifier = Modifier + .exposedDropdownSize() + .background(Color.White) + ) { + options.forEach { selectionOption -> + DropdownMenuItem( + text = { Text(selectionOption) }, + onClick = { + selectedOptionText = selectionOption + isExpanded = false + }, + modifier = Modifier.background(Color.White) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt index 4588ae8..e100c96 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/HomeScreen.kt @@ -19,7 +19,8 @@ import com.google.firebase.auth.auth fun HomeScreen( onTableClick: () -> Unit, onMyPageClick: () -> Unit, - onMainHomeClick: () -> Unit + onMainHomeClick: () -> Unit, + onCreatePostClick: () -> Unit ) { val user = Firebase.auth.currentUser Column( diff --git a/app/src/main/java/com/example/chaining/ui/screen/MainHomeScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/MainHomeScreen.kt index 69ee445..9d35d29 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/MainHomeScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/MainHomeScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.example.chaining.R import androidx.compose.foundation.layout.* diff --git a/app/src/main/res/drawable/back_arrow.png b/app/src/main/res/drawable/back_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..a2ae9489406e215e2095c17ec89bc87ebc332670 GIT binary patch literal 521 zcmeAS@N?(olHy`uVBq!ia0vp^DImO47o^n&F#FlbT+X@A&>_ z-UENWpZijFoXnc4;%O9q;NA<~$h|EVEOiY22aF4t=X`m@!!U>0{^$Mmi+VZ@_iD!# zf4(_etb2R%2D^Q`!c`2__S@H-xwNsi=j7EFdp94;Gn8vwzu5e|Si!eb%UAj?%Xkjr z9R09BLQdpxb5cbB4}aG~M?)KpHg-}3HQGE3UAh#5cqS%wh&V5iP&z8W*mu3Z{p*uS zj0KTAf)Dnk|8i;At=k;ab8i>Nf$x1ff)7@B$Ffj{p(*u1C&+H5y&~t{)d_*oho%Uqp)Fl*m6{UtN50cJuMPz;*8LV_CM@ z@B7E_d+~QYrrWMRxn+Qkk=RpPbGPr_jw>4d(n&VV_ubw{f9jWxS^vK0<&0grZ44il hdfRwTT66EQ{289E<>waFeFw%ZgQu&X%Q~loCIBtZ;&K20 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/down_arrow.png b/app/src/main/res/drawable/down_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..30c62425358867252dec423118ad1993b0775075 GIT binary patch literal 1209 zcmV;q1V;ObP)Ea?k`RCK$JG2W zk3Z%=GBrh!N1=%D7e(X|kworS)Gko;Yxux=TDW?6RpNFn)c>me1w zx`jL}MHvosl*d&Fi+0Bo zQa9GxBLIA(QaDP+cZ0#;C}{$ct5gU_3As_41i-5*g-bm<-jb$C)AU3DKdTh(E#sH5 z_FPp7fVC=wdka}Bp>WRaOY&ot!vAD^<};Cy7-P=Prv}`F*8<54BnC*{6G0#@WC6HW zV!=-h07yfJ!f<5=gTcWP6Zy+LyC<+Sb_2LZVua)~p#x&`^{m87mSso!cON1M$u(5l z{W7=241ilB2+B27w@Diy`9=g`xti-8X$x7Fol5ez2tspp6o5ZSo*``lz{U;>*(hzp zPay#Kw1YCH{c^Unk^Ftdze9(}>T03i?_VTshU5bs>W&Ac?aa^59|B-5beLRzeLX!r zeVB9sW6aeZm{wUOU4rCu5rpbW*7MRe^5Z&wh#*`yX8kxfH+P(L5t6q@5U?Aw?vO6i zvV#exD%-IhE5?}9+VGYyfSrDMS|$Pies(9`A@2(LPWmrHht8#Irq}BoAd>`ui(}kL z4`5fn-@i;IkrVAf#gpCJ7WjQlS1bZ6(rXL?2a{|#~&T}0R^dw1gAb_L+A&73rdaRJV)K6j5ykq@}(Bo_s0G_N-$Wx)m*E;Ot z0R`XIAmsa*nVF+QkFS+jYj3Jrh%x5o(Bo<~Bww%aR`qu1@w6Ue%!+(!W4S_pt?aj; zCLNL+DiE?E^f)?$TRfoPlV!;G+&Q;j=y7z4G)>Pdxs!gO&@b62<3nPp%|+H-M*o`M>@DI=}r>Q4~c{6h%=KMXKXp X{`L(YDzXN500000NkvXXu0mjf^JOTw literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/favorite_spot.png b/app/src/main/res/drawable/favorite_spot.png new file mode 100644 index 0000000000000000000000000000000000000000..c8319a91dd1e18f381c3f73a7136a8df9ad98aaa GIT binary patch literal 1695 zcmV;Q24MM#P)3-OT)Ezx$id&g`Fa94jj;D=RB2D=RB2D=RB2D=RAlwDVBo zBEb3%2aSPE?Pk~p_t8L}wt&^|NUoE17T>e1!BzR? zKI5+eat@3)R&PGi1)uTHD;N!SG7b-;dz0E%ZGKoE&eIk!I+9o?5-YxESpo}deLnJ^ z)9nD71Ej!dG&Xc379^I<5c(S?CO2npXJT@5TsEOjaZotjk{8tbedMRJ9+kl8ole99 z$d6@=<4B%%L{=qd2R|={O{W_g8JpSSGdh{wjHT4xT{u6GpU)IS$Y0IVmMAAN;=8@}t6{hZG&9q3Z8ftu6zMUQn>)`#eW#vyA13w>P>`c~*t| z(WG*t)y5`3eq3S7+ik^#2J&OHI&~>9npAM(8`b>63+OK84nqD|+4(VAZEW(9&X$57 zz-U6jjSCe$PV@{WsTm@Kimrtw2? zLw1_CbhK3)8&lWEqT78&vx@o-W{M9D%$s%f`*TN1A{?AU~`y<%6_Ufl2)Ek*f0Xp7yj;_wa$*HGp(VdK9N%iEA1^ ze8#_$1o1-_Zpb8N@vad&6g1H2gHm&-jH^+9Q(0;U+cKIS^QX3C5T3|4h`*W zKBJ?pVscI7$BLkk-dz_%kpC$g1|8G+(JZ(6{W?6oe$Pm}^qg;k~mLKIZ1WdYqu7@Gq&Ogdxh->*#E<-%ak8&9TmLGWxi6Fi( zwQ3WO3%73w*ZTU0m=HryztiajxtjAstd{$Xe=2TD_gM6}qq{tf`LSrm@-k8nm;##e zW3?GQV+v?E|B#uXy(zAS{Agu{)~0xx@guDn+L{6y@gu7lTABiy@FTAoTAAW%!jE{; zr+Asd|HIyl>cQDX^V8h*-!*v}@FQhroDr)SaXZN0)HYL~9XMVUD&CtRWUHReW zG^6yU=*SPZO*2Yuif;UH3o@gWrs&2Gw_r0$XNqq87#W+{0*rsF$ll)+8GlAv4r$(J z^sLiu>Bx_M@c=BNqlspOwLU=pz9bXH=WisLQJOU^-T2XL%E}ijESMp_QyCI}-)u^m z%+QS=a#NPgNMp#7DP>!F*Nq>tC3r84Zf*W7jUn;x+I|06(bAD0X_VHG(n(i-^rRuW z^P}es=>>lDlp(#!4{Z&pFnTiS*+gHqAw83)9r1S=vN}8U{zUOttjW(^mf$a1t*or9 ptgNi8tgNi8tgNi8tgM{k^dF^QfuH6uE4}~#002ovPDHLkV1kfNOA-J8 literal 0 HcmV?d00001 From 446eb7f88d88591cf8de54066a2a668a9e757b92 Mon Sep 17 00:00:00 2001 From: lhs2257 Date: Wed, 20 Aug 2025 17:37:55 +0900 Subject: [PATCH 042/224] =?UTF-8?q?[Fix]=20=EB=AA=A8=EC=A7=91=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=ED=99=94=EB=A9=B4=20=EB=93=9C=EB=A1=AD?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/example/chaining/ui/screen/CreatePostScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/example/chaining/ui/screen/CreatePostScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/CreatePostScreen.kt index a5ac871..0385fbf 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/CreatePostScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/CreatePostScreen.kt @@ -39,7 +39,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.chaining.R -@OptIn(ExperimentalMaterial3Api::class) @Composable fun CreatePostScreen() { var title by remember { mutableStateOf("") } // 제목을 저장할 상태 변수 From c7b1ac8522bf6a378ccbcdba59dc25eae6eb90b4 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Wed, 20 Aug 2025 18:08:29 +0900 Subject: [PATCH 043/224] =?UTF-8?q?[Design]=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/ui/screen/MyPageScreen.kt | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt index a4eadda..fbb3924 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt @@ -3,6 +3,7 @@ package com.example.chaining.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -10,6 +11,7 @@ import androidx.compose.foundation.clickable 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 @@ -328,26 +330,60 @@ fun ActionButtons( Column { Button( onClick = { /* 모집 현황 */ }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(vertical = 14.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color.White), + shape = RoundedCornerShape(14.dp), + border = BorderStroke( + width = 1.dp, + color = Color(0xFF637387) + ) ) { - Text("모집 현황") + Text( + text = "모집 현황", + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + color = Color(0xFF637387) + ) } Spacer(modifier = Modifier.height(8.dp)) Button( onClick = { /* 지원 현황 */ }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(vertical = 14.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color.White), + shape = RoundedCornerShape(12.dp), + border = BorderStroke( + width = 1.dp, + color = Color(0xFF637387) + ) ) { - Text("지원 현황") + Text( + text = "지원 현황", + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + color = Color(0xFF637387) + ) } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.weight(1f)) + Button( onClick = onSave, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF3578E5)) + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + contentPadding = PaddingValues(vertical = 14.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF3387E5)), + shape = RoundedCornerShape(12.dp) ) { - Text("프로필 저장", color = Color.White) + Text( + "프로필 저장", + color = Color.White, + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) } } } \ No newline at end of file From 1cd59b87341604eafc0e6c80c587bdbb91a1e1e6 Mon Sep 17 00:00:00 2001 From: hi2242 Date: Thu, 21 Aug 2025 13:58:10 +0900 Subject: [PATCH 044/224] =?UTF-8?q?[Feat]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=91=EC=8B=9C=20=EB=B2=84=ED=8A=BC=20=EB=B0=8F=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=EC=B0=BD=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/ui/component/TestButton.kt | 206 ++++++++++++++++++ .../chaining/ui/screen/MyPageScreen.kt | 8 +- app/src/main/res/drawable/test_profile.png | Bin 0 -> 2953 bytes 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/example/chaining/ui/component/TestButton.kt create mode 100644 app/src/main/res/drawable/test_profile.png diff --git a/app/src/main/java/com/example/chaining/ui/component/TestButton.kt b/app/src/main/java/com/example/chaining/ui/component/TestButton.kt new file mode 100644 index 0000000..e62b6dd --- /dev/null +++ b/app/src/main/java/com/example/chaining/ui/component/TestButton.kt @@ -0,0 +1,206 @@ +package com.example.chaining.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +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 androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.chaining.R +import com.example.chaining.domain.model.LanguagePref +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TestButton( + preferredLanguages: List +) { + val languageText = if (preferredLanguages.isNotEmpty()) { + preferredLanguages.joinToString(" · ") { + "${it.language} Lv.${it.level}" + } + } else { + "선호하는 언어 수준 (테스트 미응시)" + } + + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val coroutineScope = rememberCoroutineScope() + var isSheetOpen by remember { mutableStateOf(false) } + + Button( + onClick = { isSheetOpen = true }, + modifier = Modifier + .fillMaxWidth() + .background(Color.White) + .border( + width = 1.dp, + color = Color(0xFF637387), + shape = RoundedCornerShape(12.dp) + ), + contentPadding = PaddingValues( + vertical = 14.dp, + horizontal = 12.dp + ), + colors = ButtonDefaults.buttonColors(containerColor = Color.White) + ) { + Icon( + painter = painterResource(id = R.drawable.voice_recognition), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .height(24.dp) + .width(24.dp) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = languageText, + color = Color(0xFF637387), + fontSize = 14.sp, + modifier = Modifier + .weight(1f), + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + } + + if (isSheetOpen) { + ModalBottomSheet( + onDismissRequest = { isSheetOpen = false }, + sheetState = sheetState, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + containerColor = Color.White + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "언어 테스트 현황", + style = MaterialTheme.typography.titleMedium, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(8.dp)) + + val supportedLanguages = listOf("한국어", "영어", "일본어", "중국어") + + supportedLanguages.forEach { language -> + val pref = preferredLanguages.find { it.language == language } + LanguageTestItem( + language = language, + level = pref?.level, + onTestClick = { + // TODO: 테스트 화면 이동 + // navController.navigate("test/$language") + coroutineScope.launch { + sheetState.hide() + isSheetOpen = false + } + } + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} + +@Composable +fun LanguageTestItem( + language: String, + level: Int?, + onTestClick: () -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = language, + color = Color(0xFF637387), + fontSize = 16.sp + ) + + Spacer(modifier = Modifier.height(6.dp)) + + if (level != null && level > 0) { + LinearProgressIndicator( + progress = level / 10f, + modifier = Modifier + .fillMaxWidth() + .height(8.dp), + color = Color(0xFF637387), + trackColor = Color(0xFFE0E0E0) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "Lv.$level / 10", + fontSize = 12.sp, + color = Color.Gray + ) + } else { + Text( + text = "테스트 미응시", + fontSize = 12.sp, + color = Color.Gray + ) + } + } + + Spacer(modifier = Modifier.width(12.dp)) + + Button( + onClick = onTestClick, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (level != null && level > 0) Color(0xFF637387) else Color( + 0xFF4CAF50 + ) + ), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + ) { + Text( + text = if (level != null && level > 0) "재응시" else "응시하기", + color = Color.White, + fontSize = 14.sp + ) + } + } +} diff --git a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt index fbb3924..6572ded 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt @@ -54,6 +54,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.rememberAsyncImagePainter import com.example.chaining.R import com.example.chaining.domain.model.User +import com.example.chaining.ui.component.TestButton import com.example.chaining.viewmodel.UserViewModel @Composable @@ -99,6 +100,12 @@ fun MyPageScreen( leadingIconRes = R.drawable.forest_path, placeholder = "선호 여행지 선택" ) + Spacer(modifier = Modifier.height(12.dp)) + userState?.preferredLanguages?.let { + TestButton( + preferredLanguages = it + ) + } Spacer(modifier = Modifier.height(24.dp)) ActionButtons( @@ -213,7 +220,6 @@ fun DropDownField( onItemSelected: (String) -> Unit, // 선택 시 콜백 leadingIconRes: Int, // 아이콘 리소스 placeholder: String, // Placeholder 텍스트 - modifier: Modifier = Modifier ) { var expanded by remember { mutableStateOf(false) } val rotation by animateFloatAsState( diff --git a/app/src/main/res/drawable/test_profile.png b/app/src/main/res/drawable/test_profile.png new file mode 100644 index 0000000000000000000000000000000000000000..637064317e4b19d50f492758d457680d0639984a GIT binary patch literal 2953 zcmV;43wHF0P)2j&*CaLMqLh+qK@eZNsexd1KQu7w6=?03*sBBZZJ(+>Q34D4Hk2@*Ab1jY9D*t4#VY>?KBVFqa?{FOZ7=>1@<}Lec7x z@s&?%c0XRQsQ4_QC!bMcD>eUKDBh5~x2XB$Sae#oUmTBLXcimy@uZ^7P;Sug5EE1T z+m5@br1?r2(jhZFS5)+Nr|b_U6^I2wDE>T*_mGYg<5RDw9~F7`qh6*c(Xld&4^Lhj zF0Fc^)bvX?68|bDrk)q{5=Bg>it_kl_4eg7eZeD)?a_S8Z1$^FyQeOR9))S_{Kl<4 zb9D;3`~p91g{F@-yH6+<1s%V2c6(>rl_9;Y(QlX~Qu7a7-+v27eO5a+vMfFTUca=y z*5%2OHx#aUIDE38&wy1Of0h52#Ro`DH@eyLomuTOTnqR0ewK7nN7U-aO3mA(qAv!0 z28=W;9yQk{rk*S`-|p=9idpH14C6)42ya8on*42aGT9cviRwBBWD|?LK?YVc_!Hq#*11h#VR{qZ0>bRO!Y2`&q7*l zK^Pxa?MHl!8yt7TmIrx5X#S>~Lg&?VMwd@Ep_}G6bt=;--zcvTmfz6+b0(TGN))R6 z&Q9AEnnulHS1Ql1xvuCNPKlF2o)sOMj#46 zm4?RRLy@LWE7!Ox))8NtRz{&j<8H)UmR1I`LuZ65pHh{{C;FsFkOzdKQ{9VRTfI8H z5A2@Qd|PU{Zq>tTn^p!^XJ@lErFp)vr2IHjYEb->q-k+gz-e1@$Oxh6Yj&FONX>Q4 z%dncIm1{>ePa_GNx^ELx`}>tM&GU9u=UC|YJvDMQeb|lc4+dEPJA?Wxge6^5xDYz&B-Ccz;(Dd_;agm=xv^lfpJea`{ipUsRRsZJ2_^gqQ4XA9{E8f% z&~ypv7r4jVhc-1`;`4XLfezzEc-AXlC{k%%t48mYS%KY{!Xr_%pD##R=2ZEdEX*lG z3DLX^1Sa>g>oYmNc}`)#kCHq(HEL`bU01PRrIjxUaso)1-i``QKSmj#=Zi?5Zp@I) za9r*Ewr-GnAk#CkQ0Glsy{mU)q@r)77E+-=l<3%6A`xxu#F3Vo)_PJ8Qb<9U0NH4x zQwJFQYP?gD<{>msWOk%|C?^yzsSo9lQ4ZRGA^D&jp!nV_iGz0GDtK7YcNa>0bX$GY zys}v+Cp4Xzx--ZYJ0X`QS1cQN&%EL}Zr?;Ou2Gc=iK0NuFL=N)ebndO5b_vxqghXlfaPV~AOoC2IrucY0 zOQ;%QiZ(VXJsMhV*t{GGH^q(K+q*%iY- zySWzDjnGngei~}W@K|(U&|`Gchf4US_)eeP$PH5S*BcRUq~am7HFYlR1WNje;CI(8 zE(qIN+01N>hVkJ(qDaQZBII|aWoPvYo}HDXx1liQ%+y_7Bc1zFBod*)UW6eq#Xu=G ziyn=4ts(3X_l|X*jHq72mo*(zth+udwrKfGbx}_q+=yhJJ3%oAIFk7ur3jR4J;}UV z6}~+?V>VEmFLr%$YnR{V0P0%MXE3p`;+vaqbBLGET4zj6!e?0&Z%8HzpwPLp(WaV! zr7LE=&m*e*e4*(M=e4hEmfk9ae1_2R@w3`FmkDY{`ABZ{Lb9P}{%XNyr|qQCi4W#66XQN+10m4Xi4 z-RxipY`!-8bQ69gMy;vuwsF1!jyb_r*5ox)r)jd~-xSSpCXGLKMe#K_lP2g#j9NeN zvsL~IUEL0ozuBP^jc6qP(1~`kJZ`j`tq@Lu=k}wAum|i}SZ+!i>Kd(WWvp!|6)#RI zr5TN6!Nkg!$&gQLCd=Ft#)nBoUrro*vI)uz<`QJM<{+fT{UJD(zQ*gIfNr#B>o?fC zid_X&!je$@{;Wmzo}ySi1gCoRlZguA<*1uvnNp_2s^TMs<|)sja3c<`)tXj^{mePY zkPbnWzY;oCm3as39CC0d?x2(kx*7~U6a=RiAtJ6hMys%UJLsy)ffrT9ZN^bg-DE%X z91*HzIzC=%`UK7ioS6^Uk}J5|>> Date: Fri, 22 Aug 2025 11:55:56 +0900 Subject: [PATCH 045/224] =?UTF-8?q?[Design]=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EA=B8=B0=EB=B3=B8=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/repository/ProfileRepository.kt | 44 ++++++++++++++++++ .../chaining/ui/screen/MyPageScreen.kt | 15 ++++-- app/src/main/res/drawable/test_profile.png | Bin 2953 -> 5514 bytes 3 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/example/chaining/data/repository/ProfileRepository.kt diff --git a/app/src/main/java/com/example/chaining/data/repository/ProfileRepository.kt b/app/src/main/java/com/example/chaining/data/repository/ProfileRepository.kt new file mode 100644 index 0000000..8e18ac3 --- /dev/null +++ b/app/src/main/java/com/example/chaining/data/repository/ProfileRepository.kt @@ -0,0 +1,44 @@ +//package com.example.chaining.data.repository +// +//import android.net.Uri +//import com.google.firebase.auth.ktx.auth +//import com.google.firebase.database.ktx.database +//import com.google.firebase.ktx.Firebase +//import com.google.firebase.storage.ktx.storage +//import kotlinx.coroutines.tasks.await +// +//class ProfileRepository { +// +// private val storage = Firebase.storage +// private val database = Firebase.database.reference +// private val userId get() = Firebase.auth.currentUser?.uid ?: "" +// +// // 1. 이미지 업로드 +// suspend fun uploadProfileImage(imageUri: Uri): String? { +// return try { +// val ref = storage.reference.child("profileImages/$userId.jpg") +// ref.putFile(imageUri).await() +// ref.downloadUrl.await().toString() +// } catch (e: Exception) { +// e.printStackTrace() +// null +// } +// } +// +// // 2. 다운로드 URL DB 저장 +// suspend fun saveProfileImageUrl(url: String) { +// database.child("users").child(userId).child("profileImageUrl").setValue(url).await() +// } +// +// // 3. DB에서 URL 가져오기 +// suspend fun getProfileImageUrl(): String? { +// return try { +// val snapshot = +// database.child("users").child(userId).child("profileImageUrl").get().await() +// snapshot.getValue(String::class.java) +// } catch (e: Exception) { +// e.printStackTrace() +// null +// } +// } +//} diff --git a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt index 6572ded..551344c 100644 --- a/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt +++ b/app/src/main/java/com/example/chaining/ui/screen/MyPageScreen.kt @@ -17,13 +17,12 @@ 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.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.DropdownMenu @@ -128,18 +127,24 @@ fun ProfileSection( Column(horizontalAlignment = Alignment.CenterHorizontally) { Box { Image( - painter = rememberAsyncImagePainter(model = user?.profileImageUrl ?: ""), + painter = rememberAsyncImagePainter( + model = user?.profileImageUrl.takeIf { !it.isNullOrEmpty() } + ?: R.drawable.test_profile + ), contentDescription = null, modifier = Modifier .size(80.dp) .clip(CircleShape) ) Icon( - imageVector = Icons.Default.Refresh, + painter = painterResource(id = R.drawable.change), contentDescription = "프로필 변경", + tint = Color.Unspecified, modifier = Modifier + .align(Alignment.BottomEnd) - .background(Color.White, CircleShape) + .offset(x = (-8).dp, y = (-8).dp) + .size(20.dp) ) } diff --git a/app/src/main/res/drawable/test_profile.png b/app/src/main/res/drawable/test_profile.png index 637064317e4b19d50f492758d457680d0639984a..62630c478cb36c82f4ea9702ee1c02cb1609efdc 100644 GIT binary patch literal 5514 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 literal 2953 zcmV;43wHF0P)2j&*CaLMqLh+qK@eZNsexd1KQu7w6=?03*sBBZZJ(+>Q34D4Hk2@*Ab1jY9D*t4#VY>?KBVFqa?{FOZ7=>1@<}Lec7x z@s&?%c0XRQsQ4_QC!bMcD>eUKDBh5~x2XB$Sae#oUmTBLXcimy@uZ^7P;Sug5EE1T z+m5@br1?r2(jhZFS5)+Nr|b_U6^I2wDE>T*_mGYg<5RDw9~F7`qh6*c(Xld&4^Lhj zF0Fc^)bvX?68|bDrk)q{5=Bg>it_kl_4eg7eZeD)?a_S8Z1$^FyQeOR9))S_{Kl<4 zb9D;3`~p91g{F@-yH6+<1s%V2c6(>rl_9;Y(QlX~Qu7a7-+v27eO5a+vMfFTUca=y z*5%2OHx#aUIDE38&wy1Of0h52#Ro`DH@eyLomuTOTnqR0ewK7nN7U-aO3mA(qAv!0 z28=W;9yQk{rk*S`-|p=9idpH14C6)42ya8on*42aGT9cviRwBBWD|?LK?YVc_!Hq#*11h#VR{qZ0>bRO!Y2`&q7*l zK^Pxa?MHl!8yt7TmIrx5X#S>~Lg&?VMwd@Ep_}G6bt=;--zcvTmfz6+b0(TGN))R6 z&Q9AEnnulHS1Ql1xvuCNPKlF2o)sOMj#46 zm4?RRLy@LWE7!Ox))8NtRz{&j<8H)UmR1I`LuZ65pHh{{C;FsFkOzdKQ{9VRTfI8H z5A2@Qd|PU{Zq>tTn^p!^XJ@lErFp)vr2IHjYEb->q-k+gz-e1@$Oxh6Yj&FONX>Q4 z%dncIm1{>ePa_GNx^ELx`}>tM&GU9u=UC|YJvDMQeb|lc4+dEPJA?Wxge6^5xDYz&B-Ccz;(Dd_;agm=xv^lfpJea`{ipUsRRsZJ2_^gqQ4XA9{E8f% z&~ypv7r4jVhc-1`;`4XLfezzEc-AXlC{k%%t48mYS%KY{!Xr_%pD##R=2ZEdEX*lG z3DLX^1Sa>g>oYmNc}`)#kCHq(HEL`bU01PRrIjxUaso)1-i``QKSmj#=Zi?5Zp@I) za9r*Ewr-GnAk#CkQ0Glsy{mU)q@r)77E+-=l<3%6A`xxu#F3Vo)_PJ8Qb<9U0NH4x zQwJFQYP?gD<{>msWOk%|C?^yzsSo9lQ4ZRGA^D&jp!nV_iGz0GDtK7YcNa>0bX$GY zys}v+Cp4Xzx--ZYJ0X`QS1cQN&%EL}Zr?;Ou2Gc=iK0NuFL=N)ebndO5b_vxqghXlfaPV~AOoC2IrucY0 zOQ;%QiZ(VXJsMhV*t{GGH^q(K+q*%iY- zySWzDjnGngei~}W@K|(U&|`Gchf4US_)eeP$PH5S*BcRUq~am7HFYlR1WNje;CI(8 zE(qIN+01N>hVkJ(qDaQZBII|aWoPvYo}HDXx1liQ%+y_7Bc1zFBod*)UW6eq#Xu=G ziyn=4ts(3X_l|X*jHq72mo*(zth+udwrKfGbx}_q+=yhJJ3%oAIFk7ur3jR4J;}UV z6}~+?V>VEmFLr%$YnR{V0P0%MXE3p`;+vaqbBLGET4zj6!e?0&Z%8HzpwPLp(WaV! zr7LE=&m*e*e4*(M=e4hEmfk9ae1_2R@w3`FmkDY{`ABZ{Lb9P}{%XNyr|qQCi4W#66XQN+10m4Xi4 z-RxipY`!-8bQ69gMy;vuwsF1!jyb_r*5ox)r)jd~-xSSpCXGLKMe#K_lP2g#j9NeN zvsL~IUEL0ozuBP^jc6qP(1~`kJZ`j`tq@Lu=k}wAum|i}SZ+!i>Kd(WWvp!|6)#RI zr5TN6!Nkg!$&gQLCd=Ft#)nBoUrro*vI)uz<`QJM<{+fT{UJD(zQ*gIfNr#B>o?fC zid_X&!je$@{;Wmzo}ySi1gCoRlZguA<*1uvnNp_2s^TMs<|)sja3c<`)tXj^{mePY zkPbnWzY;oCm3as39CC0d?x2(kx*7~U6a=RiAtJ6hMys%UJLsy)ffrT9ZN^bg-DE%X z91*HzIzC=%`UK7ioS6^Uk}J5|>> Date: Fri, 22 Aug 2025 17:41:17 +0900 Subject: [PATCH 046/224] =?UTF-8?q?[Feat]=20=EB=AA=A8=EC=A7=91=EA=B8=80=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chaining/domain/model/RecruitPost.kt | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/example/chaining/domain/model/RecruitPost.kt b/app/src/main/java/com/example/chaining/domain/model/RecruitPost.kt index 6b6c325..86f70f5 100644 --- a/app/src/main/java/com/example/chaining/domain/model/RecruitPost.kt +++ b/app/src/main/java/com/example/chaining/domain/model/RecruitPost.kt @@ -2,18 +2,19 @@ package com.example.chaining.domain.model @kotlinx.serialization.Serializable data class RecruitPost( - val id: String = "", - val title: String = "", - val travelStyle: String = "", // 선호 여행지 스타일 - val travelDate: String = "", // 여행 일자 - val withCar: Boolean = false, // 자차 여부 - val deadline: String = "", // 모집 마감일 + val projectId: String = "", + val title: String = "", // 제목 + val preferredDestinations: String = "", // 선호 여행지 스타일 + val preferredLocations: LocationPref = LocationPref(), // 선호 여행지 or 장소 + val tourAt: Long = 0L, // 여행 일자 + val hasCar: String = "", // 자차 여부 + val closeAt: Long = 0L, // 모집 마감일 val preferredLanguages: List = emptyList(), // 선호하는 언어 정보 - val content: String = "", // 모집글 내용 - val createdAt: Long = 0L, // 작성 시각 - val kakaoOpenChatUrl: String = "", // 카톡 오픈채팅 링크 - val owner: UserSummary = UserSummary(), // 작성자 프로필 (간단 정보) - val applicants: List = emptyList() // 지원자 리스트 + val content: String = "", // 모집글 내용 + val createdAt: Long = 0L, // 작성 시각 + val kakaoOpenChatUrl: String = "", // 카톡 오픈채팅 링크 + val owner: UserSummary = UserSummary(), // 작성자 프로필 (간단 정보) + val applications: List = emptyList() // 지원자 리스트 ) @kotlinx.serialization.Serializable @@ -21,4 +22,10 @@ data class UserSummary( // 간단 버전 (닉네임/사진 정도만) val id: String = "", val nickname: String = "", val profileImageUrl: String = "" +) + +@kotlinx.serialization.Serializable +data class LocationPref( + val type: String = "", + val location: String = "" ) \ No newline at end of file From 62dbc70b6b0d81cc861d7b47a49e369494556e9e Mon Sep 17 00:00:00 2001 From: lhs2257 Date: Sun, 24 Aug 2025 15:23:21 +0900 Subject: [PATCH 047/224] =?UTF-8?q?[Design]=20=EC=8B=A0=EC=B2=AD=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=84=A4?= =?UTF-8?q?=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B7=B8=EB=9E=98?= =?UTF-8?q?=ED=94=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/other.xml | 11 +++++++++++ .../com/example/chaining/ui/navigation/NavGraph.kt | 7 ++++++- .../java/com/example/chaining/ui/navigation/Screen.kt | 1 + .../java/com/example/chaining/ui/screen/HomeScreen.kt | 8 +++++++- .../com/example/chaining/ui/screen/JoinPostScreen.kt | 5 +++++ 5 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/example/chaining/ui/screen/JoinPostScreen.kt diff --git a/.idea/other.xml b/.idea/other.xml index 457b2de..48548b9 100644 --- a/.idea/other.xml +++ b/.idea/other.xml @@ -741,6 +741,17 @@