-
Notifications
You must be signed in to change notification settings - Fork 92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HW-1 네이버 영화 검색 앱 #510
HW-1 네이버 영화 검색 앱 #510
Changes from 47 commits
6817584
a4d9f43
3bb1185
ca73e51
8bf334d
28953fa
4ef3b1b
86c099b
41c9bac
ed3f290
3002b81
1e6a939
e4ab36d
8bd02d2
c7077f0
81cbb1a
df70301
699bc80
61a23dd
9f24cae
8b3b686
4f7037d
82b7684
82c6f60
8bb5664
4eabbe0
1465352
d78e16f
1a6c38b
304729c
99958df
a649a84
c0b4c89
7991155
04ed9c0
44251a2
64031a3
aed5aba
644d523
844e1d6
d2d2572
459bb9e
c6cdef9
24796da
f4cd558
207092d
cf952ff
fb3830f
87f2a2e
f82dc00
35d017a
254cb98
e0b3a4a
15bb680
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package com.mtjin.androidarchitecturestudy.api | ||
|
||
import okhttp3.OkHttpClient | ||
import retrofit2.Retrofit | ||
import retrofit2.converter.gson.GsonConverterFactory | ||
|
||
object ApiClient { | ||
private const val BASE_URL = "https://openapi.naver.com/" | ||
fun getApiClient(): Retrofit { | ||
return Retrofit.Builder() | ||
.baseUrl(BASE_URL) | ||
.client(provideOkHttpClient(AppInterceptor())) | ||
.addConverterFactory(GsonConverterFactory.create()) | ||
.build() | ||
} | ||
|
||
private fun provideOkHttpClient( | ||
interceptor: AppInterceptor | ||
): OkHttpClient = OkHttpClient.Builder() | ||
.run { | ||
addInterceptor(interceptor) | ||
build() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.mtjin.androidarchitecturestudy.api | ||
|
||
import com.mtjin.androidarchitecturestudy.data.MovieResponse | ||
import retrofit2.Call | ||
import retrofit2.http.GET | ||
import retrofit2.http.Query | ||
|
||
interface ApiInterface { | ||
@GET("v1/search/movie.json") | ||
fun getSearchMovie( | ||
@Query("query") query: String | ||
): Call<MovieResponse> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package com.mtjin.androidarchitecturestudy.api | ||
|
||
import okhttp3.Interceptor | ||
import okhttp3.Response | ||
import java.io.IOException | ||
|
||
class AppInterceptor : Interceptor { | ||
@Throws(IOException::class) | ||
override fun intercept(chain: Interceptor.Chain) | ||
: Response = with(chain) { | ||
val newRequest = request().newBuilder().run { | ||
addHeader("X-Naver-Client-Id", "33chRuAiqlSn5hn8tIme") | ||
addHeader("X-Naver-Client-Secret", "fyfwt9PCUN") | ||
build() | ||
} | ||
proceed(newRequest) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package com.mtjin.androidarchitecturestudy.data | ||
|
||
|
||
import com.google.gson.annotations.SerializedName | ||
|
||
data class Movie( | ||
@SerializedName("actor") | ||
val actor: String, | ||
@SerializedName("director") | ||
val director: String, | ||
@SerializedName("image") | ||
val image: String, | ||
@SerializedName("link") | ||
val link: String, | ||
@SerializedName("pubDate") | ||
val pubDate: String, | ||
@SerializedName("subtitle") | ||
val subtitle: String, | ||
@SerializedName("title") | ||
val title: String, | ||
@SerializedName("userRating") | ||
val userRating: String | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package com.mtjin.androidarchitecturestudy.data | ||
|
||
|
||
import com.google.gson.annotations.SerializedName | ||
|
||
data class MovieResponse( | ||
@SerializedName("display") | ||
val display: Int, | ||
@SerializedName("items") | ||
val movies: List<Movie>, | ||
@SerializedName("lastBuildDate") | ||
val lastBuildDate: String, | ||
@SerializedName("start") | ||
val start: Int, | ||
@SerializedName("total") | ||
val total: Int | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package com.mtjin.androidarchitecturestudy.ui | ||
|
||
import android.content.Intent | ||
import android.net.Uri | ||
import android.os.Bundle | ||
import android.widget.Button | ||
import android.widget.EditText | ||
import android.widget.Toast | ||
import androidx.appcompat.app.AppCompatActivity | ||
import androidx.recyclerview.widget.RecyclerView | ||
import com.mtjin.androidarchitecturestudy.R | ||
import com.mtjin.androidarchitecturestudy.api.ApiClient | ||
import com.mtjin.androidarchitecturestudy.api.ApiInterface | ||
import com.mtjin.androidarchitecturestudy.data.Movie | ||
import com.mtjin.androidarchitecturestudy.data.MovieResponse | ||
import retrofit2.Call | ||
import retrofit2.Callback | ||
import retrofit2.Response | ||
|
||
|
||
class MainActivity : AppCompatActivity(), | ||
MovieAdapter.ItemClickListener { | ||
|
||
private lateinit var etInput: EditText | ||
private lateinit var btnSearch: Button | ||
private lateinit var rvMovies: RecyclerView | ||
private lateinit var movieAdapter: MovieAdapter | ||
private lateinit var movieCall: Call<MovieResponse> | ||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
setContentView(R.layout.activity_main) | ||
|
||
initView() | ||
initListener() | ||
} | ||
|
||
fun initView() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. private 추가해주세요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 추가했습니다. |
||
etInput = findViewById(R.id.et_input) | ||
btnSearch = findViewById(R.id.btn_search) | ||
rvMovies = findViewById(R.id.rv_movies) | ||
movieAdapter = MovieAdapter() | ||
rvMovies.adapter = movieAdapter | ||
} | ||
|
||
private fun initListener() { | ||
//어댑터 아이템 클릭리스너 | ||
movieAdapter.setItemClickListener(this) | ||
//검색버튼 | ||
btnSearch.setOnClickListener { | ||
val query = etInput.text.toString().trim() | ||
if (query.isEmpty()) { | ||
onToastMessage("검색어를 입력해주세요.") | ||
} else { | ||
onToastMessage("잠시만 기다려주세요.") | ||
requestMovie(query) | ||
} | ||
} | ||
} | ||
|
||
private fun requestMovie(query: String) { | ||
movieAdapter.clear() | ||
val apiInterface = ApiClient.getApiClient().create(ApiInterface::class.java) | ||
movieCall = apiInterface.getSearchMovie(query) | ||
movieCall.enqueue(object : Callback<MovieResponse> { | ||
override fun onFailure(call: Call<MovieResponse>, t: Throwable) { | ||
onToastMessage("불러오는데 실패 했습니다.") | ||
} | ||
|
||
override fun onResponse( | ||
call: Call<MovieResponse>, | ||
response: Response<MovieResponse> | ||
) { | ||
with(response) { | ||
if (isSuccessful && body() != null) { | ||
body()?.movies?.let { it -> movieAdapter.setItems(it) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if에서 body()가 null이 아닌걸 검사했는데 내부에서 ?로 사용하고 있네요 val body = body()
if (isSuccessful && body != null) {
body.movies.let { movieAdapter.setItems(it) }
movieAdapter.notifyDataSetChanged()
} else {
onToastMessage("불러오는데 실패 했습니다.")
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정했습니다. |
||
movieAdapter.notifyDataSetChanged() | ||
} else { | ||
onToastMessage("불러오는데 실패 했습니다.") | ||
} | ||
} | ||
} | ||
}) | ||
} | ||
|
||
override fun onItemClick(movie: Movie) { | ||
Intent(Intent.ACTION_VIEW, Uri.parse(movie.link)).takeIf { | ||
it.resolveActivity(packageManager) != null | ||
}?.run(this::startActivity) | ||
} | ||
|
||
private fun onToastMessage(message: String) { | ||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show() | ||
} | ||
|
||
override fun onDestroy() { | ||
super.onDestroy() | ||
movieCall?.cancel() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스택오버플로우 답변 참조 movieCall이 lateinit var로 선언되어 있어서 ?이 의미가 없어요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 감사합니다. 확인해보겠습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mtjin 네 이것만 처리되면 approve할게요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정했는데 movieCall을 lateinit말고 ?= null 로 해놓는게 더 낫나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mtjin lateinit을 쓸지 Nullable을 쓸지에 대해선 제 생각에는 개인 취향일 것 같아요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sooakim 넵 리뷰 감사합니다~ |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package com.mtjin.androidarchitecturestudy.ui | ||
|
||
import android.view.LayoutInflater | ||
import android.view.View | ||
import android.view.ViewGroup | ||
import android.widget.ImageView | ||
import android.widget.RatingBar | ||
import android.widget.TextView | ||
import androidx.core.text.HtmlCompat | ||
import androidx.recyclerview.widget.RecyclerView | ||
import com.bumptech.glide.Glide | ||
import com.mtjin.androidarchitecturestudy.R | ||
import com.mtjin.androidarchitecturestudy.data.Movie | ||
|
||
class MovieAdapter : | ||
RecyclerView.Adapter<MovieAdapter.ViewHolder>() { | ||
private lateinit var clickListener: ItemClickListener | ||
private val items: ArrayList<Movie> = ArrayList() | ||
|
||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { | ||
val view: View = LayoutInflater.from(parent.context) | ||
.inflate(R.layout.item_movie, parent, false) | ||
val viewHolder = ViewHolder(view) | ||
view.setOnClickListener { | ||
clickListener.onItemClick(items[viewHolder.adapterPosition]) | ||
} | ||
return viewHolder | ||
} | ||
|
||
override fun getItemCount(): Int = items.size | ||
|
||
override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||
items[position].let { | ||
holder.bind(it) | ||
} | ||
} | ||
|
||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { | ||
|
||
private val ivPoster = itemView.findViewById<ImageView>(R.id.iv_poster) | ||
private val rvRating = itemView.findViewById<RatingBar>(R.id.rb_rating) | ||
private val tvTitle = itemView.findViewById<TextView>(R.id.tv_title) | ||
private val tvReleaseDate = itemView.findViewById<TextView>(R.id.tv_release_date) | ||
private val tvActor = itemView.findViewById<TextView>(R.id.tv_actor) | ||
private val tvDirector = itemView.findViewById<TextView>(R.id.tv_director) | ||
|
||
fun bind(movie: Movie) { | ||
with(movie) { | ||
Glide.with(itemView).load(image).placeholder(R.drawable.ic_default).into(ivPoster!!) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. load부터 개행해서 한 함수당 한 라인씩 나오도록 해주세요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정했습니다. |
||
rvRating.rating = (userRating.toFloatOrNull() ?: 0f) / 2 | ||
tvTitle.text = HtmlCompat.fromHtml(title, HtmlCompat.FROM_HTML_MODE_COMPACT) | ||
tvReleaseDate.text = pubDate | ||
tvActor.text = actor | ||
tvDirector.text = director | ||
} | ||
} | ||
} | ||
|
||
fun setItems(items: List<Movie>) { | ||
this.items.addAll(items) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. notify를 함수 내부에서 호출 하도록 해주세요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정했습니다. |
||
|
||
fun setItemClickListener(listener: ItemClickListener) { | ||
this.clickListener = listener | ||
} | ||
|
||
fun clear() { | ||
this.items.clear() | ||
} | ||
|
||
interface ItemClickListener { | ||
fun onItemClick(movie: Movie) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. interface말고 kotlin higher-order function으로 처리해보세요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. higher-order function로 처리하였습니다. |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Builder pattern이기 때문에 run으로 하지 않아도 됩니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
run코드 제거했습니다.
87f2a2e