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