diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 00000000..92da1bc8 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,24 @@ +# Reed 프로젝트 작업 지침 + +## 빌드 관련 + +- **빌드는 사용자가 직접 수행합니다** +- 기능 작업 완료 후 빌드를 자동으로 실행하지 마세요 (시간이 오래 걸림) +- 빌드가 필요한 경우 사용자에게 알리고 사용자가 직접 실행하도록 합니다 + +## 커밋 관련 + +- **커밋 메시지에서 Claude 관련 문구를 제거합니다** +- 다음 문구들을 커밋 메시지에 포함하지 마세요: + - `🤖 Generated with [Claude Code](https://claude.com/claude-code)` + - `Co-Authored-By: Claude Sonnet 4.5 ` +- 커밋 작업은 사용자가 직접 수행하는 경우가 많으므로, 요청받지 않은 경우 커밋하지 마세요 + +## MCP 설정 관련 + +- **Claude Code CLI의 MCP 설정 파일 위치** + - Claude Desktop이 아니라 **Claude Code CLI**를 사용 중입니다 + - MCP 설정은 `~/.claude.json`의 `projects` 섹션에서 프로젝트별로 관리됩니다 + - Claude Desktop 설정 파일(`~/Library/Application Support/Claude/claude_desktop_config.json`)을 수정하지 마세요 +- **Figma MCP 설정 경로** + - `~/.claude.json` → `projects` → `/Users/medi/AndroidStudioProjects/YeoBee-Android` → `mcpServers` → `figma` diff --git a/.claude/CODING.md b/.claude/CODING.md new file mode 100644 index 00000000..d461ee75 --- /dev/null +++ b/.claude/CODING.md @@ -0,0 +1,25 @@ +# 코딩 가이드 + +## 코드 작성 원칙 + +- 한글 주석 사용 +- Kotlin 코딩 컨벤션 준수 +- 기존 코드 스타일 유지 +- 파일 끝에 빈 줄(newline) 추가 + +## Compose 관련 + +- **Composable 함수 내 Collection 타입** + - `List`, `Set`, `Map` 등의 Collection 대신 `ImmutableList`, `ImmutableSet`, `ImmutableMap` 사용 + - `kotlinx.collections.immutable` 라이브러리 사용 + - 예시: + ```kotlin + // ❌ 사용하지 않음 + @Composable + fun TripList(trips: List) { ... } + + // ✅ 사용 + @Composable + fun TripList(trips: ImmutableList) { ... } + ``` + - 변환 시 `toImmutableList()`, `persistentListOf()` 등 사용 diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenAuthenticator.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenAuthenticator.kt index ca7104cc..87d74923 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenAuthenticator.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenAuthenticator.kt @@ -6,7 +6,6 @@ import com.ninecraft.booket.core.network.request.RefreshTokenRequest import com.ninecraft.booket.core.network.service.ReedService import com.orhanobut.logger.Logger import dev.zacsweers.metro.Inject -import dev.zacsweers.metro.Provider import dev.zacsweers.metro.SingleIn import kotlinx.coroutines.runBlocking import okhttp3.Authenticator @@ -18,38 +17,53 @@ import okhttp3.Route @Inject class TokenAuthenticator( private val tokenDataSource: TokenDataSource, - private val serviceProvider: Provider, + private val reedService: Lazy, ) : Authenticator { + private val lock = Any() + override fun authenticate(route: Route?, response: Response): Request? { - return runBlocking { - try { - val refreshToken = tokenDataSource.getRefreshToken() + // 동시 401 응답 시 중복 refresh 방지 (refresh token rotation 대응) + synchronized(lock) { + val failedToken = response.request.header("Authorization") + ?.removePrefix("Bearer ") + .orEmpty() - if (refreshToken.isBlank()) { - Logger.d("TokenAuthenticator", "No refresh token available") - tokenDataSource.clearTokens() - return@runBlocking null - } + val currentToken = runBlocking { tokenDataSource.getAccessToken() } - val refreshTokenRequest = RefreshTokenRequest(refreshToken) - val refreshResponse = serviceProvider().refreshToken(refreshTokenRequest) + // 다른 요청이 이미 토큰을 갱신한 경우, 새 토큰으로 재시도만 수행 + if (failedToken != currentToken) { + return response.request.newBuilder() + .header("Authorization", "Bearer $currentToken") + .build() + } - tokenDataSource.apply { - setAccessToken(refreshResponse.accessToken) - setRefreshToken(refreshResponse.refreshToken) - } + return runBlocking { + try { + val refreshToken = tokenDataSource.getRefreshToken() - Logger.d("TokenAuthenticator", "Token refreshed successfully") + if (refreshToken.isBlank()) { + Logger.d("No refresh token available") + tokenDataSource.clearTokens() + return@runBlocking null + } - response.request.newBuilder() - .header("Authorization", "Bearer ${refreshResponse.accessToken}") - .build() - } catch (e: Exception) { - Logger.e("TokenAuthenticator", e.message) - tokenDataSource.clearTokens() + val refreshResponse = reedService.value.refreshToken(RefreshTokenRequest(refreshToken)) + + tokenDataSource.apply { + setAccessToken(refreshResponse.accessToken) + setRefreshToken(refreshResponse.refreshToken) + } + + Logger.d("Token refreshed successfully") - // refresh token이 만료되었거나 잘못된 경우, 재시도하지 않음 - return@runBlocking null + response.request.newBuilder() + .header("Authorization", "Bearer ${refreshResponse.accessToken}") + .build() + } catch (e: Exception) { + Logger.e(e, "Token refresh failed") + tokenDataSource.clearTokens() + return@runBlocking null + } } } }