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
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class AuthPreferences {
try keychain.save(user.email, for: Keys.userEmail)
try keychain.save(user.role.rawValue, for: Keys.userRole)
try keychain.save(String(user.expiresIn), for: Keys.expiresIn)
try keychain.save(user.position, for: Keys.position)
try keychain.save(user.position.rawValue, for: Keys.position)
try keychain.save(user.workspace, for: Keys.workspace)
try keychain.save(user.branch, for: Keys.branch)
try keychain.save(String(user.agencyId), for: Keys.agencyId)
Expand Down Expand Up @@ -86,7 +86,7 @@ class AuthPreferences {
return nil
}
// Tolerate missing profile keys by defaulting to safe values
let position = (try? keychain.get(Keys.position)) ?? ""
let positionRaw = (try? keychain.get(Keys.position)) ?? ""
let workspace = (try? keychain.get(Keys.workspace)) ?? ""
let branch = (try? keychain.get(Keys.branch)) ?? ""
let userEmail = (try? keychain.get(Keys.userEmail)) ?? ""
Expand All @@ -102,7 +102,7 @@ class AuthPreferences {
accessToken: accessToken,
refreshToken: refreshToken,
expiresIn: expiresIn,
position: position,
position: UserPosition(rawValue: positionRaw) ?? .staff,
workspace: workspace,
branch: branch,
agencyId: agencyId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ extension LoginResponseDTO {
accessToken: self.accessToken,
refreshToken: self.refreshToken,
expiresIn: self.expiresIn,
position: "",
position: .staff,
workspace: "",
branch: "",
agencyId: 0,
Expand All @@ -37,7 +37,7 @@ extension GetProfileResponseDTO {
accessToken: "",
refreshToken: "",
expiresIn: 0,
position: self.position,
position: UserPosition(rawValue: self.position) ?? .staff,
workspace: self.workspace,
branch: self.branch,
agencyId: self.organizationId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,16 @@ class AuthRepositoryImpl: AuthRepository {
throw AuthError.tokenSaveFailed(error)
}

// 2) 프로필 조회 (실패 시 롤백)
// 2) 프로필 조회 (서버 반영 지연 고려하여 재시도, 최종 실패 시 롤백)
let profileUser: User
do {
let profileResponse = try await api.getProfile()
guard let profileDto = profileResponse.data else {
// rollback tokens on invalid profile response
preferences.clear()
throw AuthError.invalidResponse
profileUser = try await retry(times: 5, initialDelayMs: 300, maxDelayMs: 1500, factor: 1.8) {
let profileResponse = try await self.api.getProfile()
guard let profileDto = profileResponse.data else {
throw AuthError.invalidResponse
}
return profileDto.toModel()
}
profileUser = profileDto.toModel()
} catch {
// rollback tokens on any profile failure
preferences.clear()
Expand Down Expand Up @@ -154,3 +154,27 @@ class AuthRepositoryImpl: AuthRepository {
return try preferences.getRefreshToken()
}
}

// MARK: - Retry Helper (Exponential Backoff)
private func retry<T>(
times: Int = 5,
initialDelayMs: UInt64 = 300,
maxDelayMs: UInt64 = 1500,
factor: Double = 1.8,
_ block: @escaping () async throws -> T
) async throws -> T {
precondition(times >= 1)
var currentDelayMs = initialDelayMs
for attempt in 1..<(times) {
do {
return try await block()
} catch {
// Optional: filter retryable errors only
let ns = currentDelayMs * 1_000_000 // ms -> ns
try? await Task.sleep(nanoseconds: ns)
let next = UInt64(Double(currentDelayMs) * factor)
currentDelayMs = min(next, maxDelayMs)
}
}
return try await block()
}
2 changes: 1 addition & 1 deletion SampoomManagement/Features/Auth/Domain/Models/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct User: Equatable {
let refreshToken: String
let expiresIn: Int
// Additional profile fields merged after login
let position: String
let position: UserPosition
let workspace: String
let branch: String
let agencyId: Int
Expand Down
38 changes: 38 additions & 0 deletions SampoomManagement/Features/Auth/Domain/Models/UserPosition.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// UserPosition.swift
// SampoomManagement
//
// Created by AI on 11/5/25.
//

import Foundation

enum UserPosition: String, CaseIterable, Codable, Equatable, Hashable {
case staff = "STAFF"
case seniorStaff = "SENIOR_STAFF"
case assistantManager = "ASSISTANT_MANAGER"
case manager = "MANAGER"
case deputyGeneralManager = "DEPUTY_GENERAL_MANAGER"
case generalManager = "GENERAL_MANAGER"
case director = "DIRECTOR"
case vicePresident = "VICE_PRESIDENT"
case president = "PRESIDENT"
case chairman = "CHAIRMAN"

var displayNameKo: String {
switch self {
case .staff: return "사원"
case .seniorStaff: return "주임"
case .assistantManager: return "대리"
case .manager: return "과장"
case .deputyGeneralManager: return "차장"
case .generalManager: return "부장"
case .director: return "이사"
case .vicePresident: return "부사장"
case .president: return "사장"
case .chairman: return "회장"
}
}
}


2 changes: 1 addition & 1 deletion SampoomManagement/Features/Auth/UI/SignUpUiState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ struct SignUpUiState: UIState {

init(
name: String = "",
workspace: String = "대리점",
workspace: String = "AGENCY",
branch: String = "",
position: String = "",
email: String = "",
Expand Down
47 changes: 37 additions & 10 deletions SampoomManagement/Features/Auth/UI/SignUpView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ struct SignUpView: View {
@StateObject private var keyboardObserver = KeyboardObserver()
@State private var name = ""
@State private var branch = ""
@State private var position = ""
@State private var selectedPosition: UserPosition? = nil
@State private var email = ""
@State private var password = ""
@State private var passwordCheck = ""
Expand Down Expand Up @@ -83,15 +83,42 @@ struct SignUpView: View {
.font(.gmarketBody)
.foregroundColor(Color("Text"))
.padding(.bottom, 4)
CommonTextField(
value: $position,
placeholder: StringResources.Auth.positionPlaceholder,
isError: viewModel.uiState.positionError != nil,
errorMessage: viewModel.uiState.positionError,
onTextChange: { text in viewModel.updatePosition(text) },
submitLabel: .next,
onSubmit: { focusedField = .email }
)
Menu {
ForEach(UserPosition.allCases, id: \.self) { pos in
Button(action: {
selectedPosition = pos
viewModel.updatePosition(pos.rawValue)
focusedField = .email
}) {
Text(pos.displayNameKo)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 6)
}
}
} label: {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(selectedPosition?.displayNameKo ?? StringResources.Auth.positionPlaceholder)
.font(.gmarketBody)
.foregroundColor(selectedPosition == nil ? .gray : Color("Text"))
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
.padding(4)
.background(
RoundedRectangle(cornerRadius: 16)
.stroke(viewModel.uiState.positionError != nil ? Color.red : Color.gray.opacity(0.4), lineWidth: 1)
)
if let error = viewModel.uiState.positionError {
Text(error)
.font(.gmarketBody)
.foregroundColor(.red)
.padding(.leading, 4)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.focused($focusedField, equals: .position)

Spacer()
Expand Down
2 changes: 1 addition & 1 deletion SampoomManagement/Features/Setting/UI/SettingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ struct SettingView: View {
.font(.gmarketTitle)
.foregroundColor(.text)

Text(user.position)
Text(user.position.displayNameKo)
.font(.gmarketBody)
.foregroundColor(.textSecondary)
}
Expand Down