Skip to content
Merged
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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ dependencies {
implementation(libs.retrofit.gson.converter)
implementation(libs.okhttp)
implementation(libs.okhttp.logging.interceptor)
implementation(libs.okhttp.urlconnection)

// Coroutines
implementation(libs.kotlinx.coroutines.android)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ class TiggleMessagingService : FirebaseMessagingService() {
?: message.notification?.body
?: "새 알림이 도착했어요."

Log.d(TAG, "onMessageReceived: data=${message.data}, notif=${message.notification}")
val deepLink = message.data["link"]

Log.d(TAG, "onMessageReceived: data=${message.data}, notif=${message.notification}, " +
"title=$title, body=$body, link=$deepLink")

// Android 13+ 권한 체크
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.ssafy.tiggle.data.datasource.remote
package com.ssafy.tiggle.core.network

import android.util.Log
import com.ssafy.tiggle.data.datasource.local.AuthDataSource
import com.ssafy.tiggle.data.datasource.remote.AuthApiService
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
Expand All @@ -24,7 +24,7 @@ class AuthInterceptor @Inject constructor(
private fun stripBearer(raw: String?): String =
raw?.replaceFirst(Regex("^Bearer\\s+", RegexOption.IGNORE_CASE), "")?.trim().orEmpty()

private fun isAuthPath(url: HttpUrl) = url.encodedPath.startsWith("/auth/")
private fun isAuthPath(url: HttpUrl) = url.encodedPath.startsWith("/api/auth/")

override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
Expand All @@ -42,39 +42,28 @@ class AuthInterceptor @Inject constructor(
}
.build()

var res = chain.proceed(req)
if (res.code != 401) return res
val res = chain.proceed(req)
if (res.code != 401 && res.code != 403) return res

// 2) 401 → 재발급 (단일 실행)
val refreshed = runBlocking {
refreshMutex.withLock {
val latest = stripBearer(authDataSource.getAccessToken())
if (latest.isNotBlank() && latest != access) return@withLock true

val refresh = stripBearer(authDataSource.getRefreshToken())
if (refresh.isBlank()) return@withLock false

val cookieHeader = authDataSource.buildCookieHeaderForReissue(refresh)
Log.d("Reissue", "➡️ Cookie header(for reissue)=${cookieHeader}")

val r = api.reissueTokenByCookie(cookie = cookieHeader)
// CookieJar가 자동으로 refreshToken/JSESSIONID를 쿠키로 전송함
val r = api.reissueTokenByCookie()
if (!r.isSuccessful) {
if (r.code() == 401) {
if (r.code() == 401 || r.code() == 403) {
// INVALID_REFRESH_TOKEN 등: 회복 불가 → 세션 정리
authDataSource.clearAuthData()
}
return@withLock false
}

val newAccess = stripBearer(r.headers()["Authorization"])
val setCookies = r.headers().values("Set-Cookie")
authDataSource.saveSetCookies(setCookies)

val cookieRefresh = setCookies.firstOrNull { it.startsWith("refreshToken=") }
?.substringAfter("refreshToken=")?.substringBefore(";")

if (newAccess.isBlank() || cookieRefresh.isNullOrBlank()) return@withLock false
authDataSource.saveTokens(newAccess, cookieRefresh)
if (newAccess.isBlank()) return@withLock false
authDataSource.saveAccessToken(newAccess)
true
}
}
Expand All @@ -94,4 +83,3 @@ class AuthInterceptor @Inject constructor(
return chain.proceed(retry)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.ssafy.tiggle.core.network

import android.util.Log
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.JavaNetCookieJar
import java.net.CookieManager
import java.net.CookiePolicy

class LoggingCookieJar : CookieJar {

private val javaNetCookieJar = JavaNetCookieJar(
CookieManager().apply { setCookiePolicy(CookiePolicy.ACCEPT_ALL) }
)

companion object {
private const val TAG = "CookieJar"
}

override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
Log.d(TAG, "🍪 쿠키 저장 - URL: $url")
cookies.forEach { cookie ->
Log.d(TAG, " 저장: ${cookie.name}=${cookie.value} (secure=${cookie.secure})")
}

javaNetCookieJar.saveFromResponse(url, cookies)
}

override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookies = javaNetCookieJar.loadForRequest(url)
Log.d(TAG, "📤 쿠키 로드 - URL: $url, 개수: ${cookies.size}")
cookies.forEach { cookie ->
Log.d(TAG, " 전송: ${cookie.name}=${cookie.value}")
}
return cookies
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.ssafy.tiggle.data.datasource.remote
package com.ssafy.tiggle.core.network

import android.util.Log
import okhttp3.Interceptor
import okhttp3.MediaType
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import okio.Buffer
import org.json.JSONArray
import org.json.JSONObject
Expand Down Expand Up @@ -41,7 +40,7 @@ class PrettyHttpLoggingInterceptor : Interceptor {
}

if (requestBodyString != null) {

Log.v(TAG, "│ ${getPrettyJson(requestBodyString)}")
}
Log.i(TAG, "└─────────────────────────────────────────────────────────────────────────────")
Expand Down Expand Up @@ -69,14 +68,14 @@ class PrettyHttpLoggingInterceptor : Interceptor {
val mediaType: MediaType? = responseBody?.contentType()
val isTextBody = isTextLike(mediaType)
var bodyString: String? = null

if (isTextBody && responseBody != null) {
try {
// 안전한 방식으로 응답 본문 읽기
val source = responseBody.source()
source.request(Long.MAX_VALUE) // 전체 본문 요청
val buffer = source.buffer

val charset = responseBody.contentType()?.charset(UTF8) ?: UTF8
bodyString = buffer.clone().readString(charset)
} catch (e: Exception) {
Expand Down Expand Up @@ -143,7 +142,7 @@ class PrettyHttpLoggingInterceptor : Interceptor {
} else {
jsonString
}
} catch (e: Exception) {
} catch (_: Exception) {
jsonString // JSON 파싱 실패 시 원본 문자열 반환
}
}
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/com/ssafy/tiggle/core/utils/Formatter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ssafy.tiggle.core.utils

import java.util.Locale

object Formatter {
// 정수 금액 포맷팅: 50,000원
fun formatCurrency(amount: Long, locale: Locale = Locale.KOREA): String =
String.format(locale, "%,d원", amount)

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,53 +17,34 @@ class AuthDataSource @Inject constructor(
* - 앱 종료 후에도 유지되는 데이터로, 앱 재시작 시 자동 로그인 기능에 사용
*/

// SharedPreferences에 저장된 인증 토큰을 관리
fun saveTokens(accessToken: String, refreshToken: String) {
// SharedPreferences에 저장된 access 토큰을 관리
fun saveAccessToken(accessToken: String) {
prefs.edit {
putString(KEY_ACCESS_TOKEN, accessToken)
putString(KEY_REFRESH_TOKEN, refreshToken)
}
Log.d("AuthLocalDataSource", "✅ accessToken 저장됨: $accessToken")
Log.d("AuthLocalDataSource", "✅ refreshToken 저장됨: $refreshToken")
}

// 토큰을 가져오는 메소드
fun getAccessToken(): String? {
return prefs.getString(KEY_ACCESS_TOKEN, null)
}

// 리프레시 토큰을 가져오는 메소드
fun getRefreshToken(): String? {
return prefs.getString(KEY_REFRESH_TOKEN, null)
}
// refresh 토큰은 쿠키로만 관리됨

/**
* 쿠키 관리 클래스
* - 서버와의 통신에서 필요한 쿠키들을 관리
* - 재발급 시 필요한 Cookie 헤더 문자열을 구성
*/

// ▼ 모든 Set-Cookie 저장
fun saveSetCookies(cookies: List<String>) {
prefs.edit { putStringSet(KEY_SET_COOKIES, cookies.toSet()) }
}

// ▼ 재발급용 Cookie 헤더 조립 (JSESSIONID 등 + refreshToken)
fun buildCookieHeaderForReissue(refreshToken: String): String {
val raw = prefs.getStringSet(KEY_SET_COOKIES, emptySet())!!.toList()
val pairs = raw.map { it.substringBefore(";") } // name=value
return (pairs + "refreshToken=$refreshToken")
.distinctBy { it.substringBefore("=") }
.joinToString("; ")
}
// 쿠키는 CookieJar가 관리하므로 앱 로컬 저장 불필요


// 인증 데이터를 모두 지우는 메소드
fun clearAuthData() {
prefs.edit {
remove(KEY_ACCESS_TOKEN)
remove(KEY_REFRESH_TOKEN)
remove(KEY_SET_COOKIES)
}
}

Expand All @@ -75,7 +56,5 @@ class AuthDataSource @Inject constructor(

companion object {
private const val KEY_ACCESS_TOKEN = "access_token"
private const val KEY_REFRESH_TOKEN = "refresh_token"
private const val KEY_SET_COOKIES = "set_cookies" // 쿠키 저장 키
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import com.ssafy.tiggle.data.model.LoginRequestDto
import com.ssafy.tiggle.data.model.SignUpRequestDto
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.POST

/**
Expand Down Expand Up @@ -37,9 +36,7 @@ interface AuthApiService {
): Response<BaseResponse<Unit>>

@POST("auth/reissue")
suspend fun reissueTokenByCookie(
@Header("Cookie") cookie: String
): Response<BaseResponse<Unit>>
suspend fun reissueTokenByCookie(): Response<BaseResponse<Unit>>


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ssafy.tiggle.data.datasource.remote

import com.ssafy.tiggle.data.model.BaseResponse
import com.ssafy.tiggle.data.model.EmptyResponse
import com.ssafy.tiggle.data.model.dutchpay.request.DutchPayRequestDto
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST

interface DutchPayApiService {
@POST("/api/dutchpay/requests")
suspend fun createDutchPayRequest(
@Body request: DutchPayRequestDto
): Response<BaseResponse<EmptyResponse>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ssafy.tiggle.data.datasource.remote

import com.ssafy.tiggle.data.model.BaseResponse
import com.ssafy.tiggle.data.model.UserSummaryDto
import retrofit2.Response
import retrofit2.http.GET

/**
* 사용자 조회 API 서비스
*/
interface UserApiService {

/**
* 전체 사용자 목록 조회
*/
@GET("users/list")
suspend fun getAllUsers(): Response<BaseResponse<List<UserSummaryDto>>>
}


Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.ssafy.tiggle.data.model

import com.ssafy.tiggle.domain.entity.Department
import com.ssafy.tiggle.domain.entity.auth.Department

/**
* 학과 DTO (Data Transfer Object)
*
*
* API 응답으로 받는 학과 데이터 구조
*/
data class DepartmentDto(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.ssafy.tiggle.data.model

import com.ssafy.tiggle.domain.entity.University
import com.ssafy.tiggle.domain.entity.auth.University

/**
* 대학교 DTO (Data Transfer Object)
*
*
* API 응답으로 받는 대학교 데이터 구조
*/
data class UniversityDto(
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/ssafy/tiggle/data/model/UserDto.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.ssafy.tiggle.data.model

import com.ssafy.tiggle.domain.entity.User
import com.ssafy.tiggle.domain.entity.auth.User

/**
* 사용자 DTO (Data Transfer Object)
*
*
* API 응답으로 받는 사용자 데이터 구조
*/
data class UserDto(
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/com/ssafy/tiggle/data/model/UserSummaryDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ssafy.tiggle.data.model

import com.ssafy.tiggle.domain.entity.dutchpay.UserSummary

/**
* 사용자 요약 DTO (리스트용)
*/
data class UserSummaryDto(
val id: Long,
val name: String
) {
fun toDomain(): UserSummary = UserSummary(
id = id,
name = name
)
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.ssafy.tiggle.data.model.dutchpay.request

data class DutchPayRequestDto(
val userIds: List<Long>,
val totalAmount: Long,
val title: String,
val message: String,
val payMore: Boolean
)
Loading