diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 148fdd2..bb44937 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 2a4bbd8..40e128f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -45,4 +45,16 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
+ implementation(libs.androidx.fragment.ktx)
+ implementation (libs.retrofit)
+ implementation (libs.converter.gson)
+ implementation (libs.androidx.lifecycle.viewmodel.ktx)
+ implementation (libs.androidx.lifecycle.livedata.ktx)
+ implementation (libs.androidx.recyclerview)
+ implementation(libs.kotlin.stdlib)
+}
+android {
+ buildFeatures {
+ dataBinding = true
+ }
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 398b755..0a6d179 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android_25_2/GitHubApiService.kt b/app/src/main/java/com/example/android_25_2/GitHubApiService.kt
new file mode 100644
index 0000000..78c9493
--- /dev/null
+++ b/app/src/main/java/com/example/android_25_2/GitHubApiService.kt
@@ -0,0 +1,36 @@
+package com.example.android_25_2
+
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import retrofit2.http.GET
+import retrofit2.http.Path
+import retrofit2.http.Query
+import retrofit2.create
+
+interface GitHubApiService {
+
+ @GET("search/users")
+ suspend fun searchUsers(
+ @Query("q") query: String,
+ @Query("page") page: Int,
+ @Query("per_page") perPage: Int
+ ): UserSearchResponse
+
+ @GET("users/{username}/repos")
+ suspend fun getUserRepositories(
+ @Path("username") username: String,
+ @Query("page") page: Int,
+ @Query("per_page") perPage: Int
+ ): List
+
+ companion object {
+ fun create(): GitHubApiService {
+ val retrofit = Retrofit.Builder()
+ .baseUrl("https://api.github.com/")
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+
+ return retrofit.create()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android_25_2/MainActivity.kt b/app/src/main/java/com/example/android_25_2/MainActivity.kt
index aed359b..f55d2c0 100644
--- a/app/src/main/java/com/example/android_25_2/MainActivity.kt
+++ b/app/src/main/java/com/example/android_25_2/MainActivity.kt
@@ -1,20 +1,84 @@
package com.example.android_25_2
import android.os.Bundle
-import androidx.activity.enableEdgeToEdge
+import android.content.Intent
+import android.util.Log
+import androidx.lifecycle.ViewModelProvider
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
+import androidx.core.widget.addTextChangedListener
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.example.android_25_2.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityMainBinding
+ private lateinit var viewModel: UserSearchViewModel
+ private lateinit var adapter: UserAdapter
+ companion object {
+ const val user_name = "username"
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- enableEdgeToEdge()
- setContentView(R.layout.activity_main)
- ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
+ Log.d("MainActivity", "π onCreate μμ")
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
+
+ viewModel = ViewModelProvider(this)[UserSearchViewModel::class.java]
+
+ setupRecyclerView()
+ setupSearchView()
+ observeViewModel()
+ Log.d("MainActivity", "β
onCreate μλ£")
+ }
+
+ private fun setupRecyclerView() {
+ Log.d("MainActivity", "π¦ RecyclerView μ€μ ")
+ adapter = UserAdapter { username ->
+ val intent = Intent(this, RepositoryActivity::class.java)
+ intent.putExtra(user_name, username)
+ startActivity(intent)
+ }
+ binding.recyclerView.adapter = adapter
+ binding.recyclerView.layoutManager = LinearLayoutManager(this)
+ binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ super.onScrolled(recyclerView, dx, dy)
+
+ val layoutManager = recyclerView.layoutManager as LinearLayoutManager
+ val lastVisible = layoutManager.findLastVisibleItemPosition()
+ val totalCount = layoutManager.itemCount
+
+ if (lastVisible >= totalCount - 5) {
+ viewModel.loadMore()
+ }
+ }
+ })
+ }
+
+ private fun setupSearchView() {
+ Log.d("MainActivity", "π SearchView μ€μ ")
+ binding.editSearch.addTextChangedListener { text ->
+ val query = text.toString()
+ Log.d("MainActivity", "β¨οΈ ν
μ€νΈ λ³κ²½: '$query'")
+ viewModel.searchUsers(query)
+ }
+ Log.d("MainActivity", "β
SearchView 리μ€λ λ±λ‘ μλ£")
+ }
+
+ private fun observeViewModel() {
+ Log.d("MainActivity", "π ViewModel κ΄μ°° μμ")
+ viewModel.users.observe(this) { users ->
+ Log.d("MainActivity", "π± μ¬μ©μ λͺ©λ‘ μ
λ°μ΄νΈ: ${users.size}κ°")
+ adapter.submitList(users)
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android_25_2/Repository.kt b/app/src/main/java/com/example/android_25_2/Repository.kt
new file mode 100644
index 0000000..ad4db50
--- /dev/null
+++ b/app/src/main/java/com/example/android_25_2/Repository.kt
@@ -0,0 +1,8 @@
+package com.example.android_25_2
+
+data class Repository(
+ val id: Long,
+ val full_name: String,
+ val description: String?,
+ val html_url: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android_25_2/RepositoryActivity.kt b/app/src/main/java/com/example/android_25_2/RepositoryActivity.kt
new file mode 100644
index 0000000..c5c6071
--- /dev/null
+++ b/app/src/main/java/com/example/android_25_2/RepositoryActivity.kt
@@ -0,0 +1,64 @@
+package com.example.android_25_2
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.example.android_25_2.MainActivity.Companion.user_name
+import com.example.android_25_2.databinding.ActivityRepositoryBinding
+
+class RepositoryActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityRepositoryBinding
+ private lateinit var viewModel: RepositoryViewModel
+ private lateinit var adapter: RepositoryAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = ActivityRepositoryBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ viewModel = ViewModelProvider(this)[RepositoryViewModel::class.java]
+ val username = intent.getStringExtra(user_name) ?: ""
+
+ title = "$username's Repositories"
+
+ setupRecyclerView()
+ observeViewModel()
+
+ viewModel.loadRepositories(username)
+ }
+
+ private fun setupRecyclerView() {
+ adapter = RepositoryAdapter { url ->
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = Uri.parse(url)
+ startActivity(intent)
+ }
+
+ binding.recyclerView.adapter = adapter
+ binding.recyclerView.layoutManager = LinearLayoutManager(this)
+ binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ super.onScrolled(recyclerView, dx, dy)
+
+ val layoutManager = recyclerView.layoutManager as LinearLayoutManager
+ val lastVisible = layoutManager.findLastVisibleItemPosition()
+ val totalCount = layoutManager.itemCount
+
+ if (lastVisible >= totalCount - 5) {
+ viewModel.loadMore()
+ }
+ }
+ })
+ }
+
+ private fun observeViewModel() {
+ viewModel.repositories.observe(this) { repositories ->
+ adapter.submitList(repositories)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android_25_2/RepositoryAdapter.kt b/app/src/main/java/com/example/android_25_2/RepositoryAdapter.kt
new file mode 100644
index 0000000..7be8f56
--- /dev/null
+++ b/app/src/main/java/com/example/android_25_2/RepositoryAdapter.kt
@@ -0,0 +1,32 @@
+package com.example.android_25_2
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import com.example.android_25_2.databinding.ItemRepositoryBinding
+
+class RepositoryAdapter(
+ private val onItemClick: (String) -> Unit
+) : ListAdapter(RepositoryDiffCallback()) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RepositoryViewHolder {
+ val inflater = LayoutInflater.from(parent.context)
+ val binding = ItemRepositoryBinding.inflate(inflater, parent, false)
+ return RepositoryViewHolder(binding, onItemClick)
+ }
+
+ override fun onBindViewHolder(holder: RepositoryViewHolder, position: Int) {
+ val repository = getItem(position)
+ holder.bind(repository)
+ }
+}
+
+class RepositoryDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: Repository, newItem: Repository): Boolean {
+ return oldItem.id == newItem.id
+ }
+ override fun areContentsTheSame(oldItem: Repository, newItem: Repository): Boolean {
+ return oldItem == newItem
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android_25_2/RepositoryViewHolder.kt b/app/src/main/java/com/example/android_25_2/RepositoryViewHolder.kt
new file mode 100644
index 0000000..4a9bf71
--- /dev/null
+++ b/app/src/main/java/com/example/android_25_2/RepositoryViewHolder.kt
@@ -0,0 +1,18 @@
+package com.example.android_25_2
+
+import androidx.recyclerview.widget.RecyclerView
+import com.example.android_25_2.databinding.ItemRepositoryBinding
+
+class RepositoryViewHolder(
+ private val binding: ItemRepositoryBinding,
+ private val onItemClick: (String) -> Unit
+) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(repository: Repository) {
+ binding.repository = repository
+ binding.executePendingBindings()
+ binding.root.setOnClickListener {
+ onItemClick(repository.html_url)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android_25_2/RepositoryViewModel.kt b/app/src/main/java/com/example/android_25_2/RepositoryViewModel.kt
new file mode 100644
index 0000000..fa9cbbb
--- /dev/null
+++ b/app/src/main/java/com/example/android_25_2/RepositoryViewModel.kt
@@ -0,0 +1,50 @@
+package com.example.android_25_2
+
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
+
+class RepositoryViewModel : ViewModel() {
+
+ private val apiService = GitHubApiService.create()
+ private val _repositories = MutableLiveData>()
+ val repositories: LiveData> = _repositories
+ private var currentPage = 1
+ private var currentUsername = ""
+
+ fun loadRepositories(username: String) {
+ currentUsername = username
+ currentPage = 1
+
+ viewModelScope.launch {
+ try {
+ val repos = apiService.getUserRepositories(username, currentPage, 30)
+ _repositories.value = repos
+ } catch (e: Exception) {
+ _repositories.value = emptyList()
+ }
+ }
+ }
+
+ fun loadMore() {
+ if (currentUsername.isEmpty()) {
+ return
+ }
+
+ viewModelScope.launch {
+ try {
+ currentPage += 1
+ val repos = apiService.getUserRepositories(currentUsername, currentPage, 30)
+ val currentList = _repositories.value ?: emptyList()
+ val newList = currentList + repos
+
+ _repositories.value = newList
+ } catch (e: Exception) {
+ currentPage -= 1
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android_25_2/User.kt b/app/src/main/java/com/example/android_25_2/User.kt
new file mode 100644
index 0000000..288a2bb
--- /dev/null
+++ b/app/src/main/java/com/example/android_25_2/User.kt
@@ -0,0 +1,12 @@
+package com.example.android_25_2
+
+data class User(
+ val id: Long,
+ val login: String,
+ val html_url: String
+)
+
+data class UserSearchResponse(
+ val total_count: Int,
+ val items: List
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android_25_2/UserAdapter.kt b/app/src/main/java/com/example/android_25_2/UserAdapter.kt
new file mode 100644
index 0000000..5c73f22
--- /dev/null
+++ b/app/src/main/java/com/example/android_25_2/UserAdapter.kt
@@ -0,0 +1,32 @@
+package com.example.android_25_2
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import com.example.android_25_2.databinding.ItemUserBinding
+
+class UserAdapter(
+ private val onItemClick: (String) -> Unit
+) : ListAdapter(UserDiffCallback()) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
+ val inflater = LayoutInflater.from(parent.context)
+ val binding = ItemUserBinding.inflate(inflater, parent, false)
+ return UserViewHolder(binding, onItemClick)
+ }
+
+ override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
+ val user = getItem(position)
+ holder.bind(user)
+ }
+}
+
+class UserDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
+ return oldItem.id == newItem.id
+ }
+ override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
+ return oldItem == newItem
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android_25_2/UserSearchViewModel.kt b/app/src/main/java/com/example/android_25_2/UserSearchViewModel.kt
new file mode 100644
index 0000000..1912202
--- /dev/null
+++ b/app/src/main/java/com/example/android_25_2/UserSearchViewModel.kt
@@ -0,0 +1,66 @@
+package com.example.android_25_2
+
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+class UserSearchViewModel : ViewModel() {
+
+ private val apiService = GitHubApiService.create()
+ private val _users = MutableLiveData>()
+ val users: LiveData> = _users
+ private var searchJob: Job? = null
+ private var currentPage = 1
+ private var currentQuery = ""
+
+ fun searchUsers(query: String) {
+ Log.d("UserSearchViewModel", "π searchUsers νΈμΆ: '$query'")
+ if (query.isEmpty()) {
+ Log.d("UserSearchViewModel", "β κ²μμ΄ λΉμ΄μμ")
+ _users.value = emptyList()
+ return
+ }
+ searchJob = viewModelScope.launch {
+ Log.d("UserSearchViewModel", "β° 300ms λκΈ° μμ")
+ delay(300)
+
+ currentQuery = query
+ currentPage = 1
+
+ try {
+ Log.d("UserSearchViewModel", "π API νΈμΆ μμ: $query")
+ val response = apiService.searchUsers(query, currentPage, 30)
+ Log.d("UserSearchViewModel", "β
κ²°κ³Ό λ°μ: ${response.items.size}κ°")
+ _users.value = response.items
+ } catch (e: Exception) {
+ Log.e("UserSearchViewModel", "β μλ¬ λ°μ: ${e.message}", e)
+ _users.value = emptyList()
+ }
+ }
+ }
+
+ fun loadMore() {
+ if (currentQuery.isEmpty()) {
+ return
+ }
+
+ viewModelScope.launch {
+ try {
+ currentPage += 1
+
+ val response = apiService.searchUsers(currentQuery, currentPage, 30)
+ val currentList = _users.value ?: emptyList()
+ val newList = currentList + response.items
+
+ _users.value = newList
+ } catch (e: Exception) {
+ currentPage -= 1
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android_25_2/UserViewHolder.kt b/app/src/main/java/com/example/android_25_2/UserViewHolder.kt
new file mode 100644
index 0000000..87497be
--- /dev/null
+++ b/app/src/main/java/com/example/android_25_2/UserViewHolder.kt
@@ -0,0 +1,18 @@
+package com.example.android_25_2
+
+import androidx.recyclerview.widget.RecyclerView
+import com.example.android_25_2.databinding.ItemUserBinding
+
+class UserViewHolder(
+ private val binding: ItemUserBinding,
+ private val onItemClick: (String) -> Unit
+) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(user: User) {
+ binding.user = user
+ binding.executePendingBindings()
+ binding.root.setOnClickListener {
+ onItemClick(user.login)
+ }
+ }
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 9affce0..017b8fa 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,10 +1,34 @@
-
+ xmlns:tools="http://schemas.android.com/tools">
-
\ No newline at end of file
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_repository.xml b/app/src/main/res/layout/activity_repository.xml
new file mode 100644
index 0000000..b58a9ce
--- /dev/null
+++ b/app/src/main/res/layout/activity_repository.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_repository.xml b/app/src/main/res/layout/item_repository.xml
new file mode 100644
index 0000000..a757636
--- /dev/null
+++ b/app/src/main/res/layout/item_repository.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_user.xml b/app/src/main/res/layout/item_user.xml
new file mode 100644
index 0000000..9a3d201
--- /dev/null
+++ b/app/src/main/res/layout/item_user.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index f8c6127..fb36573 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -7,4 +7,7 @@
#FF018786
#FF000000
#FFFFFFFF
+ #FACC00
+ #F99E14
+ #F5F5F5
\ 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 13379b4..306d33b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,8 @@
Android_25-2
+ searchβ¦
+ Delet
+ Sure?
+ Yes
+ No
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8162ca2..adf4592 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,24 +1,37 @@
[versions]
-agp = "8.9.1"
-kotlin = "1.9.24"
+agp = "8.9.3"
+converterGson = "2.9.0"
+fragmentKtx = "1.8.9"
+kotlin = "2.1.0"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
appcompat = "1.7.1"
-material = "1.12.0"
-activity = "1.11.0"
+kotlinStdlib = "2.2.0"
+lifecycleViewmodelKtx = "2.10.0"
+material = "1.13.0"
+activity = "1.12.2"
constraintlayout = "2.2.1"
+fragment_version = "1.8.9"
+recyclerview = "1.4.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
+androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleViewmodelKtx" }
+androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
+androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
+converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlinStdlib" }
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 = { module = "com.squareup.retrofit2:retrofit", version.ref = "converterGson" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }