Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand All @@ -20,6 +23,9 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".RepositoryActivity"
android:exported="false" />
</application>

</manifest>
36 changes: 36 additions & 0 deletions app/src/main/java/com/example/android_25_2/GitHubApiService.kt
Original file line number Diff line number Diff line change
@@ -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<Repository>

companion object {
fun create(): GitHubApiService {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개인적으로는 따로 빼두는게 더 좋을 것 같긴 합니다.
interface 내부에 실제 create 부분을 두는건 어색하네요

val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()

return retrofit.create()
}
}
}
72 changes: 68 additions & 4 deletions app/src/main/java/com/example/android_25_2/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -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 시작")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불필요한 log 같습니다.

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)
}
}
}
8 changes: 8 additions & 0 deletions app/src/main/java/com/example/android_25_2/Repository.kt
Original file line number Diff line number Diff line change
@@ -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
)
64 changes: 64 additions & 0 deletions app/src/main/java/com/example/android_25_2/RepositoryActivity.kt
Original file line number Diff line number Diff line change
@@ -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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

texts.xml 도 사용할 수 있습니다. "%1$s's Repositories" 로 선언해서 getString 으로 가져올 수 있죠


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)
}
}
}
32 changes: 32 additions & 0 deletions app/src/main/java/com/example/android_25_2/RepositoryAdapter.kt
Original file line number Diff line number Diff line change
@@ -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<Repository, RepositoryViewHolder>(RepositoryDiffCallback()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GOOD


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<Repository>() {
override fun areItemsTheSame(oldItem: Repository, newItem: Repository): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Repository, newItem: Repository): Boolean {
return oldItem == newItem
}
}
18 changes: 18 additions & 0 deletions app/src/main/java/com/example/android_25_2/RepositoryViewHolder.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
50 changes: 50 additions & 0 deletions app/src/main/java/com/example/android_25_2/RepositoryViewModel.kt
Original file line number Diff line number Diff line change
@@ -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<List<Repository>>()
val repositories: LiveData<List<Repository>> = _repositories
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 구조화 하는 이뉴는 뭘까요?

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
}
}
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/com/example/android_25_2/User.kt
Original file line number Diff line number Diff line change
@@ -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<User>
)
Loading