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
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,7 @@ dependencies {
implementation("com.launchdarkly:okhttp-eventsource:2.5.0")
implementation("com.jakewharton.threetenabp:threetenabp:1.4.4")

// social login (google)
implementation("com.google.android.gms:play-services-auth:20.7.0")

}
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
android:exported="true"
android:theme="@style/Theme.UMC_Closit">
</activity>

<meta-data
android:name="com.google.android.gms.client_id"
android:value="@string/default_web_client_id"/>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.example.umc_closit.data.remote.profile.ProfileService
import com.example.umc_closit.data.remote.timeline.TimelineService
import com.example.umc_closit.data.remote.profile.history.HistoryService
import com.example.umc_closit.data.remote.battle.BattleApiService
import com.example.umc_closit.data.remote.report.ReportService
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor

Expand Down Expand Up @@ -73,6 +74,10 @@ object RetrofitClient {
retrofit.create(PostService::class.java)
}

val reportService: ReportService by lazy {
retrofit.create(ReportService::class.java)
}

fun <T> createService(serviceClass: Class<T>): T {
return retrofit.create(serviceClass)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,15 @@ data class QuitResponse<T>(
val code: String,
val message: String,
val result: T
)
)

data class SocialLoginRequest(
val idToken: String
)

data class SocialLoginResponse(
val isSuccess: Boolean,
val code: String,
val message: String,
val result: LoginResult? // 서버에서 내려주는 로그인 결과(토큰 등)
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ interface AuthService {
@POST("api/auth/login")
fun loginUser(@Body request: LoginRequest): Call<LoginResponse>

@POST("/api/auth/oauth/{provider}/login")
fun socialLogin(
@Path("provider") provider: String,
@Body request: SocialLoginRequest
): Call<SocialLoginResponse>

@POST("/api/auth/refresh")
fun refreshToken(
@Body request: RefreshRequest
Expand All @@ -29,4 +35,6 @@ interface AuthService {
@DELETE("/api/v1/users/")
fun deleteUser(
): Call<QuitResponse<String>>


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.umc_closit.data.remote.report

import com.example.umc_closit.data.remote.BaseResponse
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST

data class ReportRequest(val content: String)

interface ReportService {
@POST("/api/v1/report")
fun report(@Body body: ReportRequest): Call<BaseResponse<String>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ data class LikeResponse(
data class LikeResult(
val isLiked: Boolean,
val postId: Int,
val clositId: String
val clositId: String,
val likeCount: Int
)

// save
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,24 @@ import com.bumptech.glide.Glide
import com.example.umc_closit.R
import com.example.umc_closit.data.remote.battle.TodayClosetItem
import com.example.umc_closit.ui.timeline.detail.DetailActivity
import com.example.umc_closit.data.remote.RetrofitClient
import com.example.umc_closit.data.remote.timeline.LikeResponse
import com.example.umc_closit.utils.TokenUtils
import android.widget.Toast

class TodayClosetAdapter : RecyclerView.Adapter<TodayClosetAdapter.ViewHolder>() {

private val itemList = mutableListOf<TodayClosetItem>()

private val isLikedMap = mutableMapOf<Int, Boolean>()
private val likeCountMap = mutableMapOf<Int, Int>()

class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val frontImage: ImageView = view.findViewById(R.id.img_front)
val backImage: ImageView = view.findViewById(R.id.img_back)
val profileImage: ImageView = view.findViewById(R.id.iv_user_profile)
val likeCountNum: TextView = view.findViewById(R.id.like_count_num)
val ivLike: ImageView = view.findViewById(R.id.iv_like)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
Expand All @@ -30,6 +39,12 @@ class TodayClosetAdapter : RecyclerView.Adapter<TodayClosetAdapter.ViewHolder>()

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = itemList[position]
val postId = item.postId
val isLiked = isLikedMap[postId] ?: false
val likeCount = likeCountMap[postId] ?: 0

holder.ivLike.setImageResource(if (isLiked) R.drawable.ic_like_on else R.drawable.ic_like_off)
holder.likeCountNum.text = likeCount.toString()

// 전면 사진 로드
Glide.with(holder.itemView.context)
Expand Down Expand Up @@ -58,6 +73,39 @@ class TodayClosetAdapter : RecyclerView.Adapter<TodayClosetAdapter.ViewHolder>()
intent.putExtra("postId", item.postId)
context.startActivity(intent)
}

holder.ivLike.setOnClickListener {
val currentlyLiked = isLikedMap[postId] ?: false
if (currentlyLiked) {
val call = { RetrofitClient.timelineService.removeLike(postId) }
TokenUtils.handleTokenRefresh(
call = call(),
onSuccess = { resp: LikeResponse ->
if (resp.isSuccess) {
isLikedMap[postId] = resp.result.isLiked
likeCountMap[postId] = resp.result.likeCount
notifyItemChanged(holder.bindingAdapterPosition)
}
},
onFailure = { /* 토스트 등 */ },
context = holder.itemView.context
)
} else {
val call = { RetrofitClient.timelineService.addLike(postId) }
TokenUtils.handleTokenRefresh(
call = call(),
onSuccess = { resp: LikeResponse ->
if (resp.isSuccess) {
isLikedMap[postId] = resp.result.isLiked
likeCountMap[postId] = resp.result.likeCount
notifyItemChanged(holder.bindingAdapterPosition)
}
},
onFailure = { /* 토스트 등 */ },
context = holder.itemView.context
)
}
}
}

override fun getItemCount(): Int = itemList.size
Expand Down
107 changes: 89 additions & 18 deletions app/src/main/java/com/example/umc_closit/ui/login/LoginActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,60 @@ import android.content.Intent
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContentProviderCompat.requireContext
import androidx.core.content.res.ResourcesCompat
import com.example.umc_closit.R
import com.example.umc_closit.data.remote.auth.LoginRequest
import com.example.umc_closit.data.remote.auth.LoginResponse
import com.example.umc_closit.data.remote.RetrofitClient
import com.example.umc_closit.data.remote.auth.SocialLoginRequest
import com.example.umc_closit.data.remote.auth.SocialLoginResponse
import com.example.umc_closit.databinding.ActivityLoginBinding
import com.example.umc_closit.ui.login.find.FindIDActivity
import com.example.umc_closit.ui.login.find.FindPasswordActivity
import com.example.umc_closit.ui.timeline.TimelineActivity
import com.example.umc_closit.utils.TokenUtils
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.tasks.Task
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response



class LoginActivity : AppCompatActivity() {

private lateinit var binding: ActivityLoginBinding
private var isPasswordVisible = false // 비밀번호 표시 여부

private lateinit var googleSignInClient: GoogleSignInClient
private val RC_SIGN_IN = 1001

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)

checkLoginStatus() // 로그인 체크
checkLoginStatus() // 자동 로그인 체크

// 1. GoogleSignInOptions 및 Client 초기화
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()
googleSignInClient = GoogleSignIn.getClient(this, gso)

// 2. 구글 로그인 아이콘 클릭 리스너
binding.googleIcon.setOnClickListener {
val signInIntent = googleSignInClient.signInIntent
startActivityForResult(signInIntent, RC_SIGN_IN)
}

// 3. 일반 로그인 버튼
binding.btnLogin.setOnClickListener {
val email = binding.passwordContainer.text.toString().trim()
val password = binding.etPassword.text.toString().trim()
Expand All @@ -49,30 +70,81 @@ class LoginActivity : AppCompatActivity() {
loginUser(email, password)
}

// 비밀번호 보기/숨기기 토글 기능
// 4. 비밀번호 보기/숨기기 토글
binding.btnTogglePassword.setOnClickListener {
togglePasswordVisibility()
}

// 회원가입 버튼 클릭 이벤트
// 5. 회원가입, 아이디/비밀번호 찾기 버튼
binding.btnRegister.setOnClickListener {
val intent = Intent(this, RegisterActivity::class.java)
startActivity(intent)
startActivity(Intent(this, RegisterActivity::class.java))
}

// 아이디 찾기 버튼 클릭 이벤트
binding.btnFindId.setOnClickListener {
val intent = Intent(this, FindIDActivity::class.java)
startActivity(intent)
startActivity(Intent(this, FindIDActivity::class.java))
}

// 비밀번호 찾기 버튼 클릭 이벤트
binding.btnFindPassword.setOnClickListener {
val intent = Intent(this, FindPasswordActivity::class.java)
startActivity(intent)
startActivity(Intent(this, FindPasswordActivity::class.java))
}
}

// 구글 로그인 결과 처리
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == RC_SIGN_IN) {
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
handleSignInResult(task)
}
}

private fun handleSignInResult(completedTask: Task<GoogleSignInAccount>) {
try {
val account = completedTask.getResult(ApiException::class.java)
val idToken = account?.idToken
if (idToken != null) {
sendIdTokenToServer(idToken)
} else {
Toast.makeText(this, "구글 토큰이 없습니다.", Toast.LENGTH_SHORT).show()
}
} catch (e: ApiException) {
Toast.makeText(this, "구글 로그인 실패: ${e.statusCode}", Toast.LENGTH_SHORT).show()
}
}

private fun sendIdTokenToServer(idToken: String) {
val api = { RetrofitClient.authService.socialLogin("GOOGLE", SocialLoginRequest(idToken)) }
api().enqueue(object : Callback<SocialLoginResponse> {
override fun onResponse(call: Call<SocialLoginResponse>, response: Response<SocialLoginResponse>) {
val code = response.code()
val raw = response.raw()
Log.d("SOCIAL_LOGIN", "HTTP $code ${raw.request.url}")
Log.d("SOCIAL_LOGIN", "headers=${response.headers()}")

if (response.isSuccessful) {
val body = response.body()
Log.d("SOCIAL_LOGIN", "body=$body")
if (body?.isSuccess == true && body.result != null) {
val result = body.result
// TokenUtils.saveTokens(this@LoginActivity, result.accessToken, result.refreshToken, result.userId)
Toast.makeText(this@LoginActivity, "서버 로그인 성공", Toast.LENGTH_SHORT).show()
startActivity(Intent(this@LoginActivity, TimelineActivity::class.java))
finish()
} else {
Log.e("SOCIAL_LOGIN", "isSuccess=${body?.isSuccess}, code=${body?.code}, message=${body?.message}, result=${body?.result}")
Toast.makeText(this@LoginActivity, "서버 로그인 실패: ${body?.message ?: "no message"}", Toast.LENGTH_SHORT).show()
}
} else {
val err = response.errorBody()?.string()
Log.e("SOCIAL_LOGIN", "errorBody=$err")
Toast.makeText(this@LoginActivity, "서버 오류: $code", Toast.LENGTH_SHORT).show()
}
}
override fun onFailure(call: Call<SocialLoginResponse>, t: Throwable) {
Log.e("SOCIAL_LOGIN", "network failure", t)
Toast.makeText(this@LoginActivity, "네트워크 오류: ${t.message}", Toast.LENGTH_SHORT).show()
}
})
}

private fun togglePasswordVisibility() {
if (isPasswordVisible) {
// 숨김 상태
Expand Down Expand Up @@ -135,7 +207,6 @@ class LoginActivity : AppCompatActivity() {
})
}


// 자동 로그인 기능 추가
private fun checkLoginStatus() {
val isLoggedIn = TokenUtils.isLoggedIn(this)
Expand All @@ -146,4 +217,4 @@ class LoginActivity : AppCompatActivity() {
finish()
}
}
}
}
Loading