Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
b30cf04
remove(): .gitkeep 파일 삭제
parkji1on Nov 5, 2025
6ebd110
feat(prop): AUTH BASE_URL 추가
parkji1on Nov 5, 2025
106f8e3
feat(auth): AuthApi 생성
parkji1on Nov 5, 2025
f8de6a7
feat(auth): AuthApi용 Retrofit 객체 생성
parkji1on Nov 5, 2025
72e7b93
feat(data): DataStore에 Acces&FCM 토큰 저장
parkji1on Nov 5, 2025
9d7263b
refac(log): 로깅툴 변경
parkji1on Nov 5, 2025
f50a5e1
feat(data): Auth Remote&Local DataSource 생성 및 연결
parkji1on Nov 5, 2025
08d9890
feat(domain): AccessToken Model 생성
parkji1on Nov 5, 2025
4614b4a
feat(domain): AuthRepository 생성 및 연결
parkji1on Nov 5, 2025
a97bcd6
feat(login): DataSource 변경
parkji1on Nov 5, 2025
03bba07
feat(interceptor): Http 요청 Header 자동 추가
parkji1on Nov 5, 2025
0e2f0ea
feat(ui): NavHostController 하위 페이지로 전달
parkji1on Nov 5, 2025
99ea762
feat(ui): CustomBottomBar 동일한 탭을 클릭 기능 추가
parkji1on Nov 5, 2025
ba52a26
fix(ui): BottomBar가 안눌리는 오류 수정
parkji1on Nov 5, 2025
745443e
feat(domain): Paging 처리를 위한 Domain 객체 생성
parkji1on Nov 5, 2025
29edff8
refac(data): HTTP 요청의 Thread 변경
parkji1on Nov 6, 2025
f420717
feat(dto): PageResponse 직렬화 값 변경 및 필드 추가
parkji1on Nov 6, 2025
ac3905e
refac(data): DataStore 요청의 Thread 변경
parkji1on Nov 6, 2025
09ba151
feat(dto): PageResponse DTO 다시 변경
parkji1on Nov 6, 2025
3802d1c
refac(data): Repository의 무거운 작업 Thread 변경
parkji1on Nov 6, 2025
2d18f2d
feat(ui): Customer 초기 화면 생성
parkji1on Nov 6, 2025
dcf2ffc
refac(data): FcmRepository의 무거운 작업 스레드 변경
parkji1on Nov 6, 2025
c75e4f1
feat(ui): Navigation Item 및 라벨 변경
parkji1on Nov 6, 2025
da5f434
fix(data): JSON 파싱 오류 해결
parkji1on Nov 6, 2025
fc00762
feat(ui): Supplier의 모든 화면에서 bottom sheet에 대한 navigation 추가
parkji1on Nov 6, 2025
f6e8fed
feat(ui): 공급사 화면 초기 구성
parkji1on Nov 6, 2025
39169e5
feat(util): 함수변경
parkji1on Nov 7, 2025
7b88032
feat(api): API 추가
parkji1on Nov 8, 2025
adcc0be
feat(dto): ToggleResponseDto 생성
parkji1on Nov 8, 2025
95bd8f0
feat(auth): Logout 구현 수정
parkji1on Nov 9, 2025
374d8ab
feat(profile): 사용자 프로필 data-domain 흐름 생성
parkji1on Nov 9, 2025
808d467
feat(im): 견적서로 주문 가능한 Item 조회 data-domain 흐름 생성
parkji1on Nov 9, 2025
3da4f90
feat(dashboard): 사용자의 최근 기록 조회 data-domain 흐름 생성
parkji1on Nov 9, 2025
01e4378
feat(mm): 발주서 검색을 위한 토글 조회 data-domain 흐름 생성
parkji1on Nov 9, 2025
c7ada5b
feat(sd): 고객사 프로필 수정 data-doamin 흐름 생성
parkji1on Nov 9, 2025
b426b65
feat(di): Repository, DataSource, Retrofit 객체 연결 및 생성
parkji1on Nov 9, 2025
bf4951e
feat(ui): 공급사를 위한 ui&viewmodel 생성
parkji1on Nov 9, 2025
c13a0dc
feat(ui): 화면 이동을 위한 SupplierSubNavigationItem 생성 및 연결
parkji1on Nov 9, 2025
9187244
feat(ui): 고객사를 위한 UI, ViewModel 생성
parkji1on Nov 9, 2025
c85545b
feat(ui): 화면이동을 위한 CustomerNavigationItem 생성 및 연결
parkji1on Nov 9, 2025
8c94f92
feat(dashboard): 대시보드 반환 Dto 수정 및 domain model 수정
parkji1on Nov 9, 2025
9485a2d
feat(ui): 고객사&공급사 홈 화면에 대시보드 출력
parkji1on Nov 9, 2025
ca8e5ba
feat(data): 재고 아이템 토글 목록 조회 반환값 변경
parkji1on Nov 9, 2025
9e38009
feat(data): 견적서 생성, 견적서 리스트 조회 Dto 변경
parkji1on Nov 9, 2025
06132a4
refact(): 불필요한 import 문 제거
parkji1on Nov 9, 2025
32c6831
feat(ui): 견적서 생성 토글 목록 생성
parkji1on Nov 9, 2025
1644be0
feat(ui): DetailScreen 요청사항에 따른 변경
parkji1on Nov 10, 2025
e58a18a
feat(dto): API 반환값 및 반환 타입 수정
parkji1on Nov 10, 2025
3f7a16c
feat(data): SdApi관련 DTO 수정
parkji1on Nov 10, 2025
1c551cf
feat(profile): 프로필 조회에 대한 data-domain 흐름 생성
parkji1on Nov 10, 2025
d073a7c
feat(ui) 공급사 프로필 화면 및 수정 화면 생성
parkji1on Nov 10, 2025
afe28a2
feat(ui): 고객사 프로필 화면 및 수정 화면 생성
parkji1on Nov 10, 2025
d8cecd3
feat(ui): 전표 상세 화면 수정
parkji1on Nov 10, 2025
c441c90
feat(dto): API Request&Response DTO 변경
parkji1on Nov 10, 2025
060ab98
feat(ui): 공급사 홈 화면에 최근 활동 추가
parkji1on Nov 10, 2025
9ab1ec5
feat(ui): 고객사 홈 화면에 알림 아이콘 추가
parkji1on Nov 10, 2025
fc63a90
feat(ui): 알림 목록 화면 추가
parkji1on Nov 10, 2025
c9ab05c
feat(data): 전표 상태 변경 data-domain 흐름 생성
parkji1on Nov 11, 2025
9394d54
feat(ui): 고객사, 공급사 전표에 상태 변경 버튼 추가
parkji1on Nov 11, 2025
1d033f6
feat(domain): 대시보드 Model 수정
parkji1on Nov 11, 2025
f1289ff
feat(ui): 최근 활동 항목에 대한 이동 구현
parkji1on Nov 11, 2025
b9f99f3
feat(ui): 고객사&공급사 홈 화면 알림 아이콘 추가
parkji1on Nov 11, 2025
f76a490
feat(ui): 알림 리스트 화면 및 각 알림 항목 세부 페이지로 연결
parkji1on Nov 11, 2025
b10cfdd
feat(ui): 전표 리스트 선택하는 기능 제거
parkji1on Nov 11, 2025
b1db31c
feat(ui): 최근 활동 표시 10 -> 5 개로 변경
parkji1on Nov 11, 2025
baab3cd
feat(ui): 읽지 않은 알림만 클릭시 읽음 처리 가능
parkji1on Nov 11, 2025
2b8b7df
feat(data): 읽지 않은 알림 갯수 조회 기능 추가
parkji1on Nov 11, 2025
dd3707e
feat(ui): 읽지 않은 알림이 존재하면 알림 아이콘 위에 표시
parkji1on Nov 11, 2025
ced6ee5
feat(data): FcmToken 등록 API 반환값 수정
parkji1on Nov 11, 2025
c3ac4fd
feat(fcm): 로그인, FCM 토큰 변경 시 서버에 토큰 등록
parkji1on Nov 11, 2025
a93939f
Merge pull request #8 from AutoEver-4Ever/refact/#8-login
parkji1on Nov 11, 2025
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
32 changes: 5 additions & 27 deletions app/src/main/java/com/autoever/everp/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,28 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.lifecycleScope
import com.autoever.everp.domain.repository.PushNotificationRepository
import com.autoever.everp.ui.MainScreen
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.autoever.everp.ui.theme.EverpTheme
import com.autoever.everp.ui.MainScreen
import com.autoever.everp.ui.navigation.AppNavGraph
import com.autoever.everp.ui.theme.EverpTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

@Inject
lateinit var notificationRepository: PushNotificationRepository

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
getFcmToken()
setContent {
EverpTheme {
MainScreen()
// Surface(modifier = Modifier.fillMaxSize()) {
// AppNavGraph()
// }
Surface(modifier = Modifier.fillMaxSize()) {
AppNavGraph()
}
}
}
}

private fun getFcmToken() {
// Repository를 통해서만 FCM 토큰 접근
// MainActivity에서는 Firebase 객체에 직접 접근하지 않음
lifecycleScope.launch {
try {
val token = notificationRepository.getToken()
Timber.tag("FCM").i("FCM Token: $token")
// TODO: 서버에 토큰 전송 또는 로컬 저장 등 필요한 작업 수행
} catch (e: Exception) {
Timber.tag("FCM").e(e, "Fetching FCM token failed")
}
}
}
}
7 changes: 6 additions & 1 deletion app/src/main/java/com/autoever/everp/auth/api/HttpUserApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import java.net.URL
* 기존 GWService.getUserInfo와 동일한 동작을 제공한다.
*/
class HttpUserApi : UserApi {
private companion object { const val TAG = "UserApi" }
private companion object {
const val TAG = "UserApi"
}

override suspend fun getUserInfo(accessToken: String): UserInfo = withContext(Dispatchers.IO) {
val url = URL(AuthEndpoint.USER_INFO)
Expand All @@ -42,6 +44,9 @@ class HttpUserApi : UserApi {
// API 응답은 { success, message, data: { ... } } 형태일 수 있으므로 data 객체를 우선 시도
val root = JSONObject(resp)
val json = root.optJSONObject("data") ?: root

Timber.tag(TAG).d("[SUCCESS] 사용자 정보 조회 성공: $json")

UserInfo(
userId = json.optString("userId").ifBlank { null },
userName = json.optString("userName").ifBlank { null },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package com.autoever.everp.auth.repository

import com.autoever.everp.auth.api.UserApi
import com.autoever.everp.auth.model.UserInfo
import timber.log.Timber
import javax.inject.Inject

class DefaultUserRepository @Inject constructor(
private val api: UserApi,
) : UserRepository {
override suspend fun fetchUserInfo(accessToken: String): UserInfo {
return api.getUserInfo(accessToken)
val userInfo = api.getUserInfo(accessToken)
Timber.d("Fetched User Info: $userInfo")
return userInfo
}
}

70 changes: 45 additions & 25 deletions app/src/main/java/com/autoever/everp/auth/session/SessionManager.kt
Original file line number Diff line number Diff line change
@@ -1,56 +1,76 @@
package com.autoever.everp.auth.session

import android.util.Log
import com.autoever.everp.common.annotation.ApplicationScope
import com.autoever.everp.data.datasource.local.AuthLocalDataSource
import com.autoever.everp.domain.model.auth.AccessToken
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.LocalDateTime
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class SessionManager @Inject constructor(
private val tokenStore: TokenStore,
private val authLocalDataSource: AuthLocalDataSource,
@ApplicationScope private val applicationScope: CoroutineScope,
) {
private val _state = MutableStateFlow<AuthState>(AuthState.Unauthenticated)
val state: StateFlow<AuthState> = _state

init {
refreshFromStore()
applicationScope.launch {
refreshFromStore()
}
}

fun refreshFromStore() {
try {
val token = tokenStore.getAccessToken()
if (token.isNullOrEmpty()) {
applicationScope.launch {
try {
val token = authLocalDataSource.getAccessToken()
if (token.isNullOrEmpty()) {
_state.value = AuthState.Unauthenticated
Timber.tag(TAG).i("[INFO] 저장소에서 토큰이 없어 비인증 상태로 설정했습니다.")
} else {
_state.value = AuthState.Authenticated(token)
Timber.tag(TAG).i("[INFO] 저장소의 토큰으로 인증 상태를 설정했습니다. (길이: ${token.length})")
}
} catch (e: Exception) {
_state.value = AuthState.Unauthenticated
Timber.tag(TAG).i("[INFO] 저장소에서 토큰이 없어 비인증 상태로 설정했습니다.")
} else {
_state.value = AuthState.Authenticated(token)
Timber.tag(TAG).i("[INFO] 저장소의 토큰으로 인증 상태를 설정했습니다. (길이: ${token.length})")
Timber.tag(TAG).e(e, "[ERROR] 저장소에서 토큰을 불러오는 중 오류가 발생했습니다: ${e.message}")
}
} catch (e: Exception) {
_state.value = AuthState.Unauthenticated
Log.e(TAG, "[ERROR] 저장소에서 토큰을 불러오는 중 오류가 발생했습니다: ${e.message}")
}
}

fun setAuthenticated(accessToken: String) {
try {
tokenStore.saveAccessToken(accessToken)
_state.value = AuthState.Authenticated(accessToken)
Timber.tag(TAG).i("[INFO] 인증 완료 상태로 전환했습니다. (토큰 길이: ${accessToken.length})")
} catch (e: Exception) {
Timber.tag(TAG).e("[ERROR] 인증 상태 설정 중 오류가 발생했습니다: ${e.message}")
applicationScope.launch {
try {
authLocalDataSource.saveAccessToken(
AccessToken(
token = accessToken,
type = "Bearer",
expiresIn = LocalDateTime.now().plusHours(1), // 기본값: 1시간 후
),
)
_state.value = AuthState.Authenticated(accessToken)
Timber.tag(TAG).i("[INFO] 인증 완료 상태로 전환했습니다. (토큰 길이: ${accessToken.length})")
} catch (e: Exception) {
Timber.tag(TAG).e(e, "[ERROR] 인증 상태 설정 중 오류가 발생했습니다: ${e.message}")
}
}
}

fun signOut() {
try {
tokenStore.clear()
_state.value = AuthState.Unauthenticated
Timber.tag(TAG).i("[INFO] 로그아웃 완료: 인증 상태를 해제했습니다.")
} catch (e: Exception) {
Timber.tag(TAG).e("[ERROR] 로그아웃 처리 중 오류가 발생했습니다: ${e.message}")
applicationScope.launch {
try {
authLocalDataSource.clearAccessToken()
_state.value = AuthState.Unauthenticated
Timber.tag(TAG).i("[INFO] 로그아웃 완료: 인증 상태를 해제했습니다.")
} catch (e: Exception) {
Timber.tag(TAG).e(e, "[ERROR] 로그아웃 처리 중 오류가 발생했습니다: ${e.message}")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.autoever.everp.common.annotation

import javax.inject.Qualifier

// 일회용 -> 절대 쓰지 말기
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ApplicationScope

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.autoever.everp.common.annotation

import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthRetrofit

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class NormalRetrofit
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.autoever.everp.data.datasource.local

import com.autoever.everp.domain.model.auth.AccessToken
import kotlinx.coroutines.flow.Flow

interface AuthLocalDataSource {
// === Access Token ===
val accessTokenFlow: Flow<String?>
suspend fun getAccessToken(): String?
suspend fun saveAccessToken(token: AccessToken)
suspend fun clearAccessToken()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.autoever.everp.data.datasource.local

import com.autoever.everp.domain.model.dashboard.DashboardWorkflows
import kotlinx.coroutines.flow.Flow

interface DashboardLocalDataSource {
fun observeWorkflows(): Flow<DashboardWorkflows?>
suspend fun setWorkflows(workflows: DashboardWorkflows)
suspend fun clear()
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.autoever.everp.data.datasource.local

import com.autoever.everp.domain.model.inventory.InventoryItemToggle
import kotlinx.coroutines.flow.Flow

interface ImLocalDataSource {
fun observeItemToggleList(): Flow<List<InventoryItemToggle>>
suspend fun setItemToggleList(items: List<InventoryItemToggle>)
suspend fun clear()
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.autoever.everp.data.datasource.local

import com.autoever.everp.domain.model.profile.Profile
import kotlinx.coroutines.flow.Flow

interface ProfileLocalDataSource {
fun observeProfile(): Flow<Profile?>
suspend fun setProfile(profile: Profile)
suspend fun clear()
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.autoever.everp.data.datasource.local

import kotlinx.coroutines.flow.Flow

interface TokenLocalDataSource {
// === Access Token ===
val accessTokenFlow: Flow<String?>
suspend fun getAccessToken(): String?
suspend fun saveAccessToken(token: String)
suspend fun clearAccessToken()

// === FCM Token ===
val fcmTokenFlow: Flow<String?>
suspend fun getFcmToken(): String?
suspend fun saveFcmToken(token: String)
suspend fun clearFcmToken()

// === Utilities ===
suspend fun clearAll()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.autoever.everp.data.datasource.local.datastore

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.autoever.everp.common.annotation.ApplicationScope
import com.autoever.everp.data.datasource.local.AuthLocalDataSource
import com.autoever.everp.domain.model.auth.AccessToken
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject

class AuthDataStoreLocalDataSourceImpl @Inject constructor(
@ApplicationContext private val appContext: Context,
@ApplicationScope private val appScope: CoroutineScope,
) : AuthLocalDataSource {

companion object {
private const val STORE_NAME = "everp_tokens"
private val KEY_ACCESS_TOKEN = stringPreferencesKey("access_token")
private val KEY_ACCESS_TOKEN_TYPE = stringPreferencesKey("access_token_type")
private val KEY_ACCESS_TOKEN_EXPIRES_IN = stringPreferencesKey("access_token_expires_in")
}

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = STORE_NAME)

override val accessTokenFlow: Flow<String?> = appContext.dataStore.data
.catch { emit(emptyPreferences()) }
.map { prefs -> prefs[KEY_ACCESS_TOKEN] }
.flowOn(Dispatchers.IO)


override suspend fun getAccessToken(): String? = accessTokenFlow.first()

suspend fun getAccessTokenWithType(): String? {
// 1. dataStore에서 Preferences를 한 번만 읽어옵니다.
val prefs = appContext.dataStore.data
.catch { emit(emptyPreferences()) }
.first()

// 2. 읽어온 Preferences 객체에서 필요한 값을 모두 꺼냅니다.
val token = prefs[KEY_ACCESS_TOKEN]
val type = prefs[KEY_ACCESS_TOKEN_TYPE]

return if (token != null && type != null) {
"$type $token"
} else {
null
}
}

override suspend fun saveAccessToken(token: AccessToken) {
appContext.dataStore.edit { prefs ->
prefs[KEY_ACCESS_TOKEN] = token.token
prefs[KEY_ACCESS_TOKEN_TYPE] = token.type
prefs[KEY_ACCESS_TOKEN_EXPIRES_IN] = token.expiresIn.toString()
}
}

override suspend fun clearAccessToken() {
appContext.dataStore.edit { prefs ->
prefs.remove(KEY_ACCESS_TOKEN)
prefs.remove(KEY_ACCESS_TOKEN_TYPE)
prefs.remove(KEY_ACCESS_TOKEN_EXPIRES_IN)
// prefs.clear()
}
}

}
Loading
Loading