diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..30bab2a --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..0897082 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..fdf8d99 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..824785d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 0000000..104e542 --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,329 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d3941a7..18fbdae 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,12 +1,20 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) + id("kotlin-parcelize") } android { namespace = "com.alom.androidstudy2" compileSdk = 34 + buildFeatures { + viewBinding = true + buildConfig = true + } + defaultConfig { applicationId = "com.alom.androidstudy2" minSdk = 26 @@ -15,6 +23,7 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + buildConfigField ("String", "API_KEY", getApiKey("API_KEY")) } buildTypes { @@ -32,7 +41,27 @@ android { } } +fun getApiKey(propertyKey: String): String { + val properties = Properties() + val localPropertiesFile = rootProject.file("local.properties") + if (localPropertiesFile.exists()) { + properties.load(localPropertiesFile.inputStream()) + } + return properties.getProperty(propertyKey) +} + dependencies { + implementation(libs.lifecycle.viewmodel.ktx) + implementation(libs.lifecycle.livedata.ktx) + implementation(libs.lifecycle.runtime.ktx) + + implementation(libs.retrofit) + implementation(libs.converter.gson) + implementation(libs.okhttp) + implementation(libs.logging.interceptor) + + implementation(libs.glide) + annotationProcessor(libs.compiler) implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0acee2a..8e4dd4f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + diff --git a/app/src/main/java/com/alom/androidstudy2/Adapter.kt b/app/src/main/java/com/alom/androidstudy2/Adapter.kt new file mode 100644 index 0000000..544fcd7 --- /dev/null +++ b/app/src/main/java/com/alom/androidstudy2/Adapter.kt @@ -0,0 +1,44 @@ +package com.alom.androidstudy2 + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.alom.androidstudy2.data.Item +import com.alom.androidstudy2.databinding.ItemSampleBinding +import com.bumptech.glide.Glide + +class Adapter: ListAdapter(diffUtil) { + inner class ItemViewHolder(private val binding: ItemSampleBinding): RecyclerView.ViewHolder(binding.root) { + fun bind(itemModel: Item) { + binding.tvTitle.text = itemModel.title + binding.tvPrice.text = itemModel.price + binding.tvTime.text = itemModel.time + Glide + .with(binding.image.context) + .load(itemModel.imageUrl) + .into(binding.image) + } + } + + companion object { + val diffUtil = object:DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { + return oldItem == newItem + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + return ItemViewHolder(ItemSampleBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(currentList[position]) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alom/androidstudy2/ApiService.kt b/app/src/main/java/com/alom/androidstudy2/ApiService.kt new file mode 100644 index 0000000..89f302e --- /dev/null +++ b/app/src/main/java/com/alom/androidstudy2/ApiService.kt @@ -0,0 +1,18 @@ +package com.alom.androidstudy2 + +import com.alom.androidstudy2.data.Request +import com.alom.androidstudy2.data.ResponseData +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +interface ApiService { + @POST("rpc/add_item1") + fun addItem ( + @Body body: Request + ): Call + + @POST("rpc/get_item1") + fun getItem(): Call +} \ No newline at end of file diff --git a/app/src/main/java/com/alom/androidstudy2/MainActivity.kt b/app/src/main/java/com/alom/androidstudy2/MainActivity.kt deleted file mode 100644 index 54ce3ce..0000000 --- a/app/src/main/java/com/alom/androidstudy2/MainActivity.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.alom.androidstudy2 - -import android.os.Bundle -import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContentView(R.layout.activity_main) - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) - insets - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/alom/androidstudy2/Repository.kt b/app/src/main/java/com/alom/androidstudy2/Repository.kt new file mode 100644 index 0000000..6f3fff1 --- /dev/null +++ b/app/src/main/java/com/alom/androidstudy2/Repository.kt @@ -0,0 +1,10 @@ +package com.alom.androidstudy2 + +import com.alom.androidstudy2.data.Item +import com.alom.androidstudy2.data.Request +import kotlinx.coroutines.flow.Flow + +interface Repository { + suspend fun getItem(): Flow> + suspend fun addItem(request: Request): Result +} \ No newline at end of file diff --git a/app/src/main/java/com/alom/androidstudy2/RepositoryImpl.kt b/app/src/main/java/com/alom/androidstudy2/RepositoryImpl.kt new file mode 100644 index 0000000..05e11bd --- /dev/null +++ b/app/src/main/java/com/alom/androidstudy2/RepositoryImpl.kt @@ -0,0 +1,37 @@ +package com.alom.androidstudy2 + +import com.alom.androidstudy2.data.Item +import com.alom.androidstudy2.data.Request +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flow +import retrofit2.await +import retrofit2.awaitResponse + +class RepositoryImpl: Repository { + override suspend fun getItem(): Flow> = flow { + try { + val response = Retrofit.instance.getItem().awaitResponse() + if (response.isSuccessful) { + emit(response.body()?.data ?: emptyList()) + } else { + emit(emptyList()) + } + } catch (e: Exception) { + emit(emptyList()) + } + } + + override suspend fun addItem(request: Request): Result { + return try { + val response = Retrofit.instance.addItem(request).awaitResponse() + if (response.isSuccessful) { + Result.success(Unit) + } else { + Result.failure(Exception("Failed to add Item")) + } + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alom/androidstudy2/Retrofit.kt b/app/src/main/java/com/alom/androidstudy2/Retrofit.kt new file mode 100644 index 0000000..86e7da1 --- /dev/null +++ b/app/src/main/java/com/alom/androidstudy2/Retrofit.kt @@ -0,0 +1,40 @@ +package com.alom.androidstudy2 + +import android.util.Log +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + + +object Retrofit { + private const val BASE_URL = "https://goaplrynweyxovekoezl.supabase.co/rest/v1/" + + val instance : ApiService by lazy { + val logging = HttpLoggingInterceptor() + logging.setLevel(HttpLoggingInterceptor.Level.BODY) + + val apiKeyInterceptor = Interceptor { chain -> + val request = chain.request().newBuilder() + .addHeader("apikey", BuildConfig.API_KEY) + .build() + chain.proceed(request) + } + + val okHttpClient = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .addInterceptor(apiKeyInterceptor) + .build() + + val retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() + retrofit.create(ApiService::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alom/androidstudy2/ViewModel.kt b/app/src/main/java/com/alom/androidstudy2/ViewModel.kt new file mode 100644 index 0000000..db0e7dc --- /dev/null +++ b/app/src/main/java/com/alom/androidstudy2/ViewModel.kt @@ -0,0 +1,47 @@ +package com.alom.androidstudy2 + +import android.util.Log +import android.widget.Toast +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.alom.androidstudy2.activity.MainActivity +import com.alom.androidstudy2.data.Item +import com.alom.androidstudy2.data.Request +import com.alom.androidstudy2.data.ResponseData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class ViewModel(private val repository:Repository) : ViewModel() { + private val _item = MutableStateFlow>(emptyList()) + val item: StateFlow> get() = _item + + init { + updateItems() + } + + fun updateItems() { + viewModelScope.launch { + repository.getItem().collect { items -> + _item.value = items + } + } + } + + fun addItem(request: Request, onSuccess: () -> Unit, onFailure: () -> Unit) { + viewModelScope.launch { + val result = repository.addItem(request) + if (result.isSuccess) { + onSuccess() + } else { + onFailure() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/alom/androidstudy2/ViewModelFactory.kt b/app/src/main/java/com/alom/androidstudy2/ViewModelFactory.kt new file mode 100644 index 0000000..c5044a9 --- /dev/null +++ b/app/src/main/java/com/alom/androidstudy2/ViewModelFactory.kt @@ -0,0 +1,13 @@ +package com.alom.androidstudy2 + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class ViewModelFactory(private val repository: Repository) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(com.alom.androidstudy2.ViewModel::class.java)) { + return ViewModel(repository) as T + } + throw IllegalArgumentException("ViewModel class not found") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alom/androidstudy2/activity/AddActivity.kt b/app/src/main/java/com/alom/androidstudy2/activity/AddActivity.kt new file mode 100644 index 0000000..21c101c --- /dev/null +++ b/app/src/main/java/com/alom/androidstudy2/activity/AddActivity.kt @@ -0,0 +1,46 @@ +package com.alom.androidstudy2.activity + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.alom.androidstudy2.Retrofit +import com.alom.androidstudy2.data.Request +import com.alom.androidstudy2.data.ResponseData +import com.alom.androidstudy2.databinding.ActivityAddBinding +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class AddActivity : AppCompatActivity() { + private lateinit var binding: ActivityAddBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityAddBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.btnBack.setOnClickListener { + finish() + } + + binding.btnSave.setOnClickListener { + val title = binding.etTitle.text.toString() + val price = binding.etPrice.text.toString() + val time = binding.etTime.text.toString() + + if (title.isEmpty() || price.isEmpty() || time.isEmpty()) { + Toast.makeText(this, "입력해라", Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + + val requestData = Request(title, price, time) + + val returnIntent = Intent() + returnIntent.putExtra("requestData", requestData) + setResult(RESULT_OK, returnIntent) + finish() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alom/androidstudy2/activity/MainActivity.kt b/app/src/main/java/com/alom/androidstudy2/activity/MainActivity.kt new file mode 100644 index 0000000..6c82d81 --- /dev/null +++ b/app/src/main/java/com/alom/androidstudy2/activity/MainActivity.kt @@ -0,0 +1,75 @@ +package com.alom.androidstudy2.activity + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewmodel.viewModelFactory +import androidx.recyclerview.widget.LinearLayoutManager +import com.alom.androidstudy2.Adapter +import com.alom.androidstudy2.RepositoryImpl +import com.alom.androidstudy2.ViewModel +import com.alom.androidstudy2.ViewModelFactory +import com.alom.androidstudy2.data.Request +import com.alom.androidstudy2.databinding.ActivityMainBinding +import kotlinx.coroutines.launch + +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + private lateinit var viewModel: ViewModel + private lateinit var adapter: Adapter + + private val addActivityLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + val requestData = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + result.data?.getParcelableExtra("requestData", Request::class.java) + } else { + result.data?.getParcelableExtra("requestData") as? Request + } + + Log.d("MainActivity", requestData.toString()) + viewModel.addItem(requestData!!, onSuccess = { onSuccess() }, onFailure = { onFailure() }) + } + } + + private fun onSuccess() { + Toast.makeText(this, "업로드 성공", Toast.LENGTH_SHORT).show() + viewModel.updateItems() + } + + private fun onFailure() { + Toast.makeText(this, "업로드 실패", Toast.LENGTH_SHORT).show() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + val intent = Intent(this, AddActivity::class.java) + + val repository = RepositoryImpl() + val factory = ViewModelFactory(repository) + + viewModel = ViewModelProvider(this, factory).get(ViewModel::class.java) + adapter = Adapter() + + binding.rcv.layoutManager = LinearLayoutManager(this) + binding.rcv.adapter = adapter + lifecycleScope.launch { + viewModel.item.collect { items -> + adapter.submitList(items) + Log.d("MainActivity", items.toString()) + } + } + + binding.btnAdd.setOnClickListener { + addActivityLauncher.launch(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alom/androidstudy2/data/Request.kt b/app/src/main/java/com/alom/androidstudy2/data/Request.kt new file mode 100644 index 0000000..4d8e7b8 --- /dev/null +++ b/app/src/main/java/com/alom/androidstudy2/data/Request.kt @@ -0,0 +1,15 @@ +package com.alom.androidstudy2.data + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Parcelize +data class Request( + @SerializedName("p_title") + val title: String, + @SerializedName("p_price") + val price: String, + @SerializedName("p_time") + val time: String +) : Parcelable diff --git a/app/src/main/java/com/alom/androidstudy2/data/ResponseData.kt b/app/src/main/java/com/alom/androidstudy2/data/ResponseData.kt new file mode 100644 index 0000000..8bc71dc --- /dev/null +++ b/app/src/main/java/com/alom/androidstudy2/data/ResponseData.kt @@ -0,0 +1,22 @@ +package com.alom.androidstudy2.data + +import com.google.gson.annotations.SerializedName + +data class ResponseData( + val result: Int, + val message: String, + val data: MutableList +) + +data class Item( + @SerializedName("id") + val id: Int, + @SerializedName("title") + val title: String, + @SerializedName("price") + val price: String, + @SerializedName("image_url") + val imageUrl: String, + @SerializedName("time") + val time: String +) diff --git a/app/src/main/res/layout/activity_add.xml b/app/src/main/res/layout/activity_add.xml new file mode 100644 index 0000000..01cb1d2 --- /dev/null +++ b/app/src/main/res/layout/activity_add.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 86a5d97..d4063a7 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,15 +5,46 @@ android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivity"> + android:background="@color/white" + tools:context=".activity.MainActivity"> - + app:layout_constraintStart_toStartOf="parent"> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_sample.xml b/app/src/main/res/layout/item_sample.xml new file mode 100644 index 0000000..a48879f --- /dev/null +++ b/app/src/main/res/layout/item_sample.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ca0ae81..b862355 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ android_study_14_2 + + Hello blank fragment \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9b2b101..c0939d8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,11 @@ appcompat = "1.7.0" material = "1.12.0" activity = "1.9.3" constraintlayout = "2.1.4" +retrofitVersion = "2.9.0" +okhttpVersion = "4.12.0" +lifecycleVersion = "2.8.4" +glideVersion = "4.16.0" +compilerVersion = "4.14.2" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -19,6 +24,17 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofitVersion"} +converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofitVersion"} +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttpVersion"} +logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttpVersion"} + +lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleVersion"} +lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleVersion"} +lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleVersion"} + +glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glideVersion" } +compiler = { group = "com.github.bumptech.glide", name = "compiler", version.ref = "compilerVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }