diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 3c78e42..b72df30 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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")
+
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0bacc66..a3cb3e8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -73,6 +73,10 @@
android:exported="true"
android:theme="@style/Theme.UMC_Closit">
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/umc_closit/data/remote/RetrofitClient.kt b/app/src/main/java/com/example/umc_closit/data/remote/RetrofitClient.kt
index 3448a1c..b372b6d 100644
--- a/app/src/main/java/com/example/umc_closit/data/remote/RetrofitClient.kt
+++ b/app/src/main/java/com/example/umc_closit/data/remote/RetrofitClient.kt
@@ -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
@@ -73,6 +74,10 @@ object RetrofitClient {
retrofit.create(PostService::class.java)
}
+ val reportService: ReportService by lazy {
+ retrofit.create(ReportService::class.java)
+ }
+
fun createService(serviceClass: Class): T {
return retrofit.create(serviceClass)
}
diff --git a/app/src/main/java/com/example/umc_closit/data/remote/auth/AuthResponse.kt b/app/src/main/java/com/example/umc_closit/data/remote/auth/AuthResponse.kt
index 216a526..71843e6 100644
--- a/app/src/main/java/com/example/umc_closit/data/remote/auth/AuthResponse.kt
+++ b/app/src/main/java/com/example/umc_closit/data/remote/auth/AuthResponse.kt
@@ -88,4 +88,15 @@ data class QuitResponse(
val code: String,
val message: String,
val result: T
-)
\ No newline at end of file
+)
+
+data class SocialLoginRequest(
+ val idToken: String
+)
+
+data class SocialLoginResponse(
+ val isSuccess: Boolean,
+ val code: String,
+ val message: String,
+ val result: LoginResult? // 서버에서 내려주는 로그인 결과(토큰 등)
+)
diff --git a/app/src/main/java/com/example/umc_closit/data/remote/auth/AuthService.kt b/app/src/main/java/com/example/umc_closit/data/remote/auth/AuthService.kt
index 046ae67..fb303d0 100644
--- a/app/src/main/java/com/example/umc_closit/data/remote/auth/AuthService.kt
+++ b/app/src/main/java/com/example/umc_closit/data/remote/auth/AuthService.kt
@@ -16,6 +16,12 @@ interface AuthService {
@POST("api/auth/login")
fun loginUser(@Body request: LoginRequest): Call
+ @POST("/api/auth/oauth/{provider}/login")
+ fun socialLogin(
+ @Path("provider") provider: String,
+ @Body request: SocialLoginRequest
+ ): Call
+
@POST("/api/auth/refresh")
fun refreshToken(
@Body request: RefreshRequest
@@ -29,4 +35,6 @@ interface AuthService {
@DELETE("/api/v1/users/")
fun deleteUser(
): Call>
+
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/umc_closit/data/remote/report/ReportService/ReprotService.kt b/app/src/main/java/com/example/umc_closit/data/remote/report/ReportService/ReprotService.kt
new file mode 100644
index 0000000..a83851b
--- /dev/null
+++ b/app/src/main/java/com/example/umc_closit/data/remote/report/ReportService/ReprotService.kt
@@ -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>
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/umc_closit/data/remote/timeline/TimelineResponse.kt b/app/src/main/java/com/example/umc_closit/data/remote/timeline/TimelineResponse.kt
index ed2cac2..6aee9f8 100644
--- a/app/src/main/java/com/example/umc_closit/data/remote/timeline/TimelineResponse.kt
+++ b/app/src/main/java/com/example/umc_closit/data/remote/timeline/TimelineResponse.kt
@@ -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
diff --git a/app/src/main/java/com/example/umc_closit/ui/community/todaycloset/TodayClosetAdapter.kt b/app/src/main/java/com/example/umc_closit/ui/community/todaycloset/TodayClosetAdapter.kt
index f7826c7..1de3ac7 100644
--- a/app/src/main/java/com/example/umc_closit/ui/community/todaycloset/TodayClosetAdapter.kt
+++ b/app/src/main/java/com/example/umc_closit/ui/community/todaycloset/TodayClosetAdapter.kt
@@ -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() {
private val itemList = mutableListOf()
+ private val isLikedMap = mutableMapOf()
+ private val likeCountMap = mutableMapOf()
+
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 {
@@ -30,6 +39,12 @@ class TodayClosetAdapter : RecyclerView.Adapter()
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)
@@ -58,6 +73,39 @@ class TodayClosetAdapter : RecyclerView.Adapter()
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
diff --git a/app/src/main/java/com/example/umc_closit/ui/login/LoginActivity.kt b/app/src/main/java/com/example/umc_closit/ui/login/LoginActivity.kt
index a25e72d..999546e 100644
--- a/app/src/main/java/com/example/umc_closit/ui/login/LoginActivity.kt
+++ b/app/src/main/java/com/example/umc_closit/ui/login/LoginActivity.kt
@@ -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()
@@ -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) {
+ 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 {
+ override fun onResponse(call: Call, response: Response) {
+ 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, t: Throwable) {
+ Log.e("SOCIAL_LOGIN", "network failure", t)
+ Toast.makeText(this@LoginActivity, "네트워크 오류: ${t.message}", Toast.LENGTH_SHORT).show()
+ }
+ })
+ }
+
private fun togglePasswordVisibility() {
if (isPasswordVisible) {
// 숨김 상태
@@ -135,7 +207,6 @@ class LoginActivity : AppCompatActivity() {
})
}
-
// 자동 로그인 기능 추가
private fun checkLoginStatus() {
val isLoggedIn = TokenUtils.isLoggedIn(this)
@@ -146,4 +217,4 @@ class LoginActivity : AppCompatActivity() {
finish()
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/umc_closit/ui/timeline/TimelineAdapter.kt b/app/src/main/java/com/example/umc_closit/ui/timeline/TimelineAdapter.kt
index 73d3d69..d9152bf 100644
--- a/app/src/main/java/com/example/umc_closit/ui/timeline/TimelineAdapter.kt
+++ b/app/src/main/java/com/example/umc_closit/ui/timeline/TimelineAdapter.kt
@@ -31,6 +31,8 @@ import com.example.umc_closit.ui.timeline.detail.DetailActivity
import com.example.umc_closit.data.remote.post.PostDeleteResponse
import com.example.umc_closit.utils.FileUtils
import com.example.umc_closit.utils.TokenUtils
+import com.example.umc_closit.databinding.DialogReportBinding
+import com.example.umc_closit.data.remote.report.ReportRequest
class TimelineAdapter(
private val context: Context,
@@ -57,8 +59,8 @@ class TimelineAdapter(
Glide.with(context).load(item.backImage).into(ivImageSmall)
Glide.with(context).load(item.profileImage).transform(CircleCrop()).into(ivUserProfile)
- ivLike.setImageResource(if (item.isLiked) R.drawable.ic_like_on else R.drawable.ic_like_off)
- ivSave.setImageResource(if (item.isSaved) R.drawable.ic_save_on else R.drawable.ic_save_off)
+ ivLike.setImageResource(if (item.isLiked) R.drawable.ic_heart_on else R.drawable.ic_heart)
+ ivSave.setImageResource(if (item.isSaved) R.drawable.ic_star_on else R.drawable.ic_star)
// 좋아요 수 binding
likeCountNum.text = item.likeCount.toString()
@@ -102,7 +104,10 @@ class TimelineAdapter(
call = apiCall(),
onSuccess = { result: LikeResponse ->
if (result.isSuccess) {
- timelineItems[position] = item.copy(isLiked = false)
+ timelineItems[position] = item.copy(
+ isLiked = result.result.isLiked,
+ likeCount = result.result.likeCount
+ )
notifyItemChanged(position)
}
},
@@ -117,7 +122,10 @@ class TimelineAdapter(
call = apiCall(),
onSuccess = { result: LikeResponse ->
if (result.isSuccess) {
- timelineItems[position] = item.copy(isLiked = true)
+ timelineItems[position] = item.copy(
+ isLiked = result.result.isLiked,
+ likeCount = result.result.likeCount
+ )
notifyItemChanged(position)
}
},
@@ -177,15 +185,19 @@ class TimelineAdapter(
}
}
-// ivOption.setOnClickListener {
-// val myClositId = TokenUtils.getClositId(context)
-// val isMyPost = (item.clositId == myClositId)
-// if (isMyPost) {
-// showDeleteModifyDialog(context, item.postId) {
-// timelineItems.removeAt(position)
-// notifyItemRemoved(position)
-// }
-// }
+ ivOption.setOnClickListener {
+ val myClositId = TokenUtils.getClositId(context)
+ val isMyPost = (item.clositId == myClositId)
+
+ if (isMyPost) {
+ showDeleteModifyDialog(context, item.postId) {
+ timelineItems.removeAt(position)
+ notifyItemRemoved(position)
+ }
+ } else {
+ showBlockReportDialog(context)
+ }
+ }
ivUserProfile.setOnClickListener {
val activity = context as? androidx.fragment.app.FragmentActivity
@@ -199,17 +211,7 @@ class TimelineAdapter(
?.commit()
}
-// ivOption.setOnClickListener {
-// val myClositId = TokenUtils.getClositId(context) //나의 Id
-// val isMyPost = (item.clositId == myClositId)
-//
-// if (isMyPost) {
-// showDeleteModifyDialog(context)
-// }
-// else {
-// showBlockReportDialog(context)
-// }
-// }
+
}
}
@@ -227,26 +229,24 @@ class TimelineAdapter(
}
//삭제 수정 dialog 표시
- fun showDeleteModifyDialog(context: Context) {
+ fun showDeleteModifyDialog(context: Context, postId: Int, onDeleteSuccess: () -> Unit) {
val dialog = Dialog(context)
val binding = DialogDelModBinding.inflate(LayoutInflater.from(context))
dialog.setContentView(binding.root)
- dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) // 배경 투명
+ dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialog.show()
- // 예시: 삭제 버튼 클릭 리스너
binding.deleteButton.setOnClickListener {
- // 삭제 로직
+// deletePost(context, postId, onDeleteSuccess)
dialog.dismiss()
}
- // 예시: 수정 버튼 클릭 리스너
+
binding.modifyButton.setOnClickListener {
// 수정 로직
dialog.dismiss()
}
}
-
//차단, 신고 dialog 표시
fun showBlockReportDialog(context: Context) {
val dialog = Dialog(context)
@@ -255,6 +255,8 @@ class TimelineAdapter(
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialog.show()
+
+
// 예시: 차단 버튼 클릭 리스너
binding.blockButton.setOnClickListener {
// 차단 보내기 로직
@@ -264,10 +266,49 @@ class TimelineAdapter(
binding.reportButton.setOnClickListener {
// 신고 로직
dialog.dismiss()
+ showReportDialog(context)
+ }
+ }
+ private fun showReportDialog(context: Context) {
+ val dialog = Dialog(context)
+ val binding = DialogReportBinding.inflate(LayoutInflater.from(context))
+ dialog.setContentView(binding.root)
+ dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ dialog.show()
+
+ val dm = context.resources.displayMetrics
+ val width = (dm.widthPixels * 0.92).toInt()
+ val height = (dm.heightPixels * 0.55).toInt() // 세로 85%
+ dialog.window?.setLayout(width, height)
+
+ binding.dialogReportButton.setOnClickListener {
+ val content = binding.etContent.text?.toString()?.trim().orEmpty()
+ if (content.isEmpty()) {
+ Toast.makeText(context, "신고 사유를 입력하세요.", Toast.LENGTH_SHORT).show()
+ return@setOnClickListener
+ }
+
+ val apiCall = { RetrofitClient.reportService.report(ReportRequest(content)) }
+
+ TokenUtils.handleTokenRefresh(
+ call = apiCall(),
+ onSuccess = { resp ->
+ if (resp.isSuccess) {
+ Toast.makeText(context, "신고가 접수되었습니다.", Toast.LENGTH_SHORT).show()
+ dialog.dismiss()
+ } else {
+ Toast.makeText(context, resp.message ?: "신고 실패", Toast.LENGTH_SHORT).show()
+ }
+ },
+ onFailure = { t ->
+ Toast.makeText(context, "네트워크 오류: ${t.message}", Toast.LENGTH_SHORT).show()
+ },
+ context = context
+ )
}
}
// fun deletePost(context: Context, postId: Int, onSuccess: () -> Unit) {
-// val apiCall = { RetrofitClient.postService.deletePost(postId) }
+// val apiCall: () -> Call = { RetrofitClient.postService.deletePost(postId) }
// TokenUtils.handleTokenRefresh(
// call = apiCall(),
// onSuccess = { response ->
diff --git a/app/src/main/java/com/example/umc_closit/ui/timeline/detail/DetailActivity.kt b/app/src/main/java/com/example/umc_closit/ui/timeline/detail/DetailActivity.kt
index f78ded7..220d507 100644
--- a/app/src/main/java/com/example/umc_closit/ui/timeline/detail/DetailActivity.kt
+++ b/app/src/main/java/com/example/umc_closit/ui/timeline/detail/DetailActivity.kt
@@ -90,8 +90,8 @@ class DetailActivity : AppCompatActivity() {
private fun updateLikeAndBookmark() {
// 좋아요/북마크 상태 업데이트
- binding.ivLike.setImageResource(if (isLiked) R.drawable.ic_like_on else R.drawable.ic_like_off)
- binding.ivSave.setImageResource(if (isSaved) R.drawable.ic_save_on else R.drawable.ic_save_off)
+ binding.ivLike.setImageResource(if (isLiked) R.drawable.ic_heart_on else R.drawable.ic_heart)
+ binding.ivSave.setImageResource(if (isSaved) R.drawable.ic_star_on else R.drawable.ic_star)
}
private fun fetchPostDetail(postId: Int) {
diff --git a/app/src/main/java/com/example/umc_closit/ui/upload/UploadFragment.kt b/app/src/main/java/com/example/umc_closit/ui/upload/UploadFragment.kt
index 8933ae4..cdcbb83 100644
--- a/app/src/main/java/com/example/umc_closit/ui/upload/UploadFragment.kt
+++ b/app/src/main/java/com/example/umc_closit/ui/upload/UploadFragment.kt
@@ -1,16 +1,31 @@
package com.example.umc_closit.ui.upload
+import android.app.Dialog
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Typeface
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
+import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
+import android.widget.TextView
+import android.widget.Toast
+import androidx.constraintlayout.helper.widget.Flow
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
+import com.example.umc_closit.R
import com.example.umc_closit.data.remote.RetrofitClient
import com.example.umc_closit.data.remote.post.ItemTag
import com.example.umc_closit.data.remote.post.PostDetail
import com.example.umc_closit.databinding.FragmentUploadBinding
+import com.example.umc_closit.databinding.CustomTagDialogBinding
import com.example.umc_closit.utils.FileUtils
import com.example.umc_closit.utils.TokenUtils
@@ -19,7 +34,10 @@ class UploadFragment : Fragment() {
private lateinit var binding: FragmentUploadBinding
private var isFrontImageBig = true // 현재 큰 이미지가 앞면인지 여부
private var isTagVisible = false // 태그 표시 여부
+ private var isColorExtractMode = false // 색상 추출 모드 여부
private lateinit var post: PostDetail // 게시글 데이터
+ private var originalBitmap: Bitmap? = null // 원본 비트맵
+ private val hashtags = mutableListOf() // 해시태그 리스트
companion object {
private const val ARG_POST_ID = "postId"
@@ -46,6 +64,19 @@ class UploadFragment : Fragment() {
val postId = arguments?.getInt(ARG_POST_ID) ?: return
loadPostDetail(postId) // 게시글 데이터 로드
+
+ // viewColorIcon 클릭 리스너 설정
+ binding.viewColorIcon.setOnClickListener {
+ isColorExtractMode = !isColorExtractMode
+ }
+
+ // btnHashtag 클릭 리스너 설정
+ binding.btnHashtag.setOnClickListener {
+ showHashtagDialog { newHashtag ->
+ hashtags.add(newHashtag)
+ createHashtagTextView(newHashtag, binding.clHashtag, binding.flowHashtagContainer)
+ }
+ }
}
// 게시글 상세 조회 후 이미지 로드
@@ -61,6 +92,9 @@ class UploadFragment : Fragment() {
Glide.with(requireContext()).load(post.frontImage).into(binding.ivImageBig)
Glide.with(requireContext()).load(post.backImage).into(binding.ivImageSmall)
+ // 비트맵 로드 (색상 추출을 위해)
+ loadBitmapForColorExtraction()
+
setupImageClickListeners()
}
},
@@ -87,6 +121,19 @@ class UploadFragment : Fragment() {
binding.ivImageSmall.setOnClickListener {
swapImagesWithTags()
}
+
+ // clTagContainer 터치 리스너 설정 (색상 추출용)
+ binding.clTagContainer.setOnTouchListener { view, event ->
+ if (isColorExtractMode) {
+ if (event.action == MotionEvent.ACTION_DOWN || event.action == MotionEvent.ACTION_MOVE) {
+ originalBitmap?.let { bmp ->
+ val color = getTouchedColor(bmp, event.x, event.y)
+ setIconColor(binding.viewColorIcon, color)
+ }
+ }
+ }
+ true
+ }
}
// 태그 보이기
@@ -128,4 +175,100 @@ class UploadFragment : Fragment() {
binding.clTagContainer.removeAllViews() // 태그 제거
}
}
+
+ // 비트맵 로드 (색상 추출을 위해)
+ private fun loadBitmapForColorExtraction() {
+ Glide.with(requireContext())
+ .asBitmap()
+ .load(post.frontImage)
+ .into(object : com.bumptech.glide.request.target.CustomTarget() {
+ override fun onResourceReady(resource: Bitmap, transition: com.bumptech.glide.request.transition.Transition?) {
+ originalBitmap = resource
+ }
+ override fun onLoadCleared(placeholder: android.graphics.drawable.Drawable?) {}
+ })
+ }
+
+ // 아이콘 색상 변경
+ private fun setIconColor(view: View, color: Int) {
+ val bg = view.background
+ if (bg is GradientDrawable) {
+ bg.setColor(color)
+ } else {
+ view.setBackgroundColor(color)
+ }
+ }
+
+ // 이미지에서 색상 추출
+ private fun getTouchedColor(bitmap: Bitmap, touchX: Float, touchY: Float): Int {
+ val ivWidth = binding.clTagContainer.width
+ val ivHeight = binding.clTagContainer.height
+
+ val bmpWidth = bitmap.width
+ val bmpHeight = bitmap.height
+
+ val xRatio = touchX / ivWidth
+ val yRatio = touchY / ivHeight
+
+ val pixelX = (xRatio * bmpWidth).toInt().coerceIn(0, bmpWidth - 1)
+ val pixelY = (yRatio * bmpHeight).toInt().coerceIn(0, bmpHeight - 1)
+
+ return bitmap.getPixel(pixelX, pixelY)
+ }
+
+ // 해시태그 입력 다이얼로그
+ private fun showHashtagDialog(onHashtagSaved: (String) -> Unit) {
+ // 다이얼로그 생성
+ val dialog = Dialog(requireContext())
+ val binding = CustomTagDialogBinding.inflate(layoutInflater)
+
+ dialog.setContentView(binding.root)
+ dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) // 배경 투명화
+
+ // 기본값으로 '#' 추가
+ binding.etHashtag.setText("#")
+ binding.etHashtag.setSelection(1) // 커서를 # 뒤로 이동
+
+ // 취소 버튼
+ binding.btnCancel.setOnClickListener {
+ dialog.dismiss()
+ }
+
+ // 확인 버튼
+ binding.btnConfirm.setOnClickListener {
+ val input = binding.etHashtag.text.toString().trim()
+
+ // 입력 검증
+ if (input.length > 1 && input.startsWith("#")) {
+ onHashtagSaved(input)
+ dialog.dismiss()
+ } else {
+ Toast.makeText(requireContext(), "올바른 해시태그를 입력하세요.", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ dialog.show()
+ }
+
+ // 해시태그 TextView 생성 및 Flow에 추가
+ private fun createHashtagTextView(text: String, parentLayout: ConstraintLayout, flow: Flow) {
+ val textView = TextView(requireContext()).apply {
+ id = View.generateViewId()
+ this.text = text
+ textSize = 16f
+ typeface = ResourcesCompat.getFont(requireContext(), R.font.noto_medium)
+ includeFontPadding = false
+ setTextColor(ContextCompat.getColor(requireContext(), R.color.white))
+ setBackgroundResource(R.drawable.bg_detail_hashtag)
+ setPadding(36, 12, 36, 12)
+
+ layoutParams = ConstraintLayout.LayoutParams(
+ ConstraintLayout.LayoutParams.WRAP_CONTENT,
+ ConstraintLayout.LayoutParams.WRAP_CONTENT
+ )
+ }
+
+ parentLayout.addView(textView)
+ flow.referencedIds += textView.id
+ }
}
diff --git a/app/src/main/res/drawable/bg_detail_hashtag.xml b/app/src/main/res/drawable/bg_detail_hashtag.xml
index 053df42..9381fba 100644
--- a/app/src/main/res/drawable/bg_detail_hashtag.xml
+++ b/app/src/main/res/drawable/bg_detail_hashtag.xml
@@ -4,6 +4,15 @@
android:viewportWidth="109"
android:viewportHeight="32">
+ android:pathData="M8,0
+ L101,0
+ A8,8 0,0 1,109 8
+ L109,24
+ A8,8 0,0 1,101 32
+ L8,32
+ A8,8 0,0 1,0 24
+ L0,8
+ A8,8 0,0 1,8 0
+ Z"
+ android:fillColor="#E4E4E4"/>
diff --git a/app/src/main/res/drawable/bg_dialog_rounded.xml b/app/src/main/res/drawable/bg_dialog_rounded.xml
new file mode 100644
index 0000000..eb3e26d
--- /dev/null
+++ b/app/src/main/res/drawable/bg_dialog_rounded.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_rounded_99.xml b/app/src/main/res/drawable/bg_rounded_99.xml
new file mode 100644
index 0000000..fc53136
--- /dev/null
+++ b/app/src/main/res/drawable/bg_rounded_99.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/btn__report_short.xml b/app/src/main/res/drawable/btn__report_short.xml
new file mode 100644
index 0000000..2d14b34
--- /dev/null
+++ b/app/src/main/res/drawable/btn__report_short.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/btn_report_block_short_pink.xml b/app/src/main/res/drawable/btn_report_block_short_pink.xml
new file mode 100644
index 0000000..e74f04f
--- /dev/null
+++ b/app/src/main/res/drawable/btn_report_block_short_pink.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_bell_button.xml b/app/src/main/res/drawable/ic_bell_button.xml
new file mode 100644
index 0000000..0ec112e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bell_button.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_heart_on.xml b/app/src/main/res/drawable/ic_heart_on.xml
new file mode 100644
index 0000000..b8322c4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_heart_on.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_star_on.xml b/app/src/main/res/drawable/ic_star_on.xml
new file mode 100644
index 0000000..f9e5279
--- /dev/null
+++ b/app/src/main/res/drawable/ic_star_on.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_upload_plus_new.xml b/app/src/main/res/drawable/ic_upload_plus_new.xml
new file mode 100644
index 0000000..f3321f0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_upload_plus_new.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/dialog_report.xml b/app/src/main/res/layout/dialog_report.xml
index 01ee476..265632b 100644
--- a/app/src/main/res/layout/dialog_report.xml
+++ b/app/src/main/res/layout/dialog_report.xml
@@ -5,7 +5,8 @@
android:id="@+id/reportContainer"
android:layout_width="340dp"
android:layout_height="383dp"
- android:background="@color/white">
+ android:background="@drawable/bg_dialog_rounded"
+ android:padding="0dp">
@@ -89,6 +90,21 @@
app:layout_constraintTop_toTopOf="@id/reportForm"
/>
+
+
@@ -97,26 +113,24 @@
android:id="@+id/dialogReportButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:src="@drawable/btn_report"
+ android:src="@drawable/btn__report_short"
android:background="@color/transparent"
- app:layout_constraintBottom_toTopOf="@id/dialogReportAndBlockButton"
- android:layout_marginBottom="10dp"
+ app:layout_constraintEnd_toStartOf="@id/dialogReportAndBlockButton"
app:layout_constraintStart_toStartOf="@id/reportContainer"
- app:layout_constraintEnd_toEndOf="@id/reportContainer"
+ app:layout_constraintBottom_toBottomOf="@id/reportContainer"
+ android:layout_marginBottom="15dp"
/>
+
-
-
-
+ app:layout_constraintEnd_toEndOf="@id/reportContainer"
+ app:layout_constraintStart_toEndOf="@id/dialogReportButton" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_timeline.xml b/app/src/main/res/layout/fragment_timeline.xml
index db1ea0e..b2bb05a 100644
--- a/app/src/main/res/layout/fragment_timeline.xml
+++ b/app/src/main/res/layout/fragment_timeline.xml
@@ -34,7 +34,7 @@
android:id="@+id/iv_notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:src="@drawable/ic_notification"
+ android:src="@drawable/ic_bell_button"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
diff --git a/app/src/main/res/layout/fragment_todaycloset.xml b/app/src/main/res/layout/fragment_todaycloset.xml
index 1712c91..e5ad74f 100644
--- a/app/src/main/res/layout/fragment_todaycloset.xml
+++ b/app/src/main/res/layout/fragment_todaycloset.xml
@@ -36,9 +36,9 @@
android:id="@+id/createButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textColor="@color/white"
+ android:textColor="@color/black"
android:background="@android:color/transparent"
- android:drawableEnd="@drawable/ic_upload_plus"
+ android:drawableEnd="@drawable/ic_upload_plus_new"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
diff --git a/app/src/main/res/layout/fragment_upload.xml b/app/src/main/res/layout/fragment_upload.xml
index ba30664..a754bca 100644
--- a/app/src/main/res/layout/fragment_upload.xml
+++ b/app/src/main/res/layout/fragment_upload.xml
@@ -19,7 +19,9 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent">
+ app:layout_constraintHeight_percent="0.8"
+ app:layout_constraintBottom_toTopOf="@id/layoutPointColor"
+ android:layout_marginBottom="10dp">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_timeline.xml b/app/src/main/res/layout/item_timeline.xml
index 40bc0e9..8a0a371 100644
--- a/app/src/main/res/layout/item_timeline.xml
+++ b/app/src/main/res/layout/item_timeline.xml
@@ -94,51 +94,56 @@
+ app:layout_constraintEnd_toEndOf="@id/cl_frame"
+ app:layout_constraintBottom_toBottomOf="@id/cl_image_container">
+ app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toTopOf="parent" />
-
+
+
+
-
+
+
+
-
+
#D3D3D3
#FAFAFA
#BABECD
+
+ #38FFFFFF
\ 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 9329b9c..8c9391e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -5,5 +5,7 @@
%1$d일째 기록중
+ 264999588415-vcafofhekom7ntnuo7r55eljed0mkp1h.apps.googleusercontent.com
+
\ No newline at end of file