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
7 changes: 4 additions & 3 deletions Projects/App/Sources/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ extension SceneDelegate: SplashViewDelegate {
else { fatalError("onboardingRepository 의존성이 등록되지 않았습니다.") }

Task { @MainActor in
let isLogined = await userDataRepository.reissueToken()
if isLogined {
let userState = await userDataRepository.reissueToken()
switch userState {
case .user:
if onboardingRepository.isOnboardingDone() {
window?.rootViewController = TabBarView()
} else {
Expand All @@ -70,7 +71,7 @@ extension SceneDelegate: SplashViewDelegate {
let navigationController = UINavigationController(rootViewController: onboardingView)
window?.rootViewController = navigationController
}
} else {
case .guest, nil:
let introView = IntroView()
let navigationController = UINavigationController(rootViewController: introView)
window?.rootViewController = navigationController
Expand Down
20 changes: 11 additions & 9 deletions Projects/DataSource/Sources/Repository/UserDataRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,26 @@ final class UserDataRepository: UserDataRepositoryProtocol {
return user.nickname
}

func reissueToken() async -> Bool {
func reissueToken() async -> UserState? {
do {
let refreshToken = try tokenManager.loadToken(tokenType: .refreshToken)
let endpoint = AuthEndpoint.reissue(refreshToken: refreshToken)

guard let tokenResponse = try await networkService.request(endpoint: endpoint, type: TokenResponseDTO.self)
else { return false }
guard let loginResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
else { return nil }

try tokenManager.saveToken(token: tokenResponse.accessToken, tokenType: .accessToken)
try tokenManager.saveToken(token: tokenResponse.refreshToken, tokenType: .refreshToken)
try tokenManager.saveToken(token: loginResponse.accessToken, tokenType: .accessToken)
try tokenManager.saveToken(token: loginResponse.refreshToken, tokenType: .refreshToken)

BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(tokenResponse.accessToken)")
BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved: \(tokenResponse.refreshToken)")
BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(loginResponse.accessToken)")
BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved: \(loginResponse.refreshToken)")
BitnagilLogger.log(logType: .debug, message: "User State: \(loginResponse.userState)")
Comment on lines +36 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

토큰 값을 그대로 로그에 출력하면 보안 위험이 큽니다

accessToken, refreshToken 을 디버그 로그에 남기면 콘솔 공유·크래시 리포트 업로드 시 유출될 수 있습니다.
운영 빌드에서는 마스킹하거나 완전히 제거해 주세요.

🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Repository/UserDataRepository.swift around lines
36 to 38, the accessToken and refreshToken are logged directly, which poses a
security risk. Modify the logging to mask or omit these token values in
production builds by conditionally checking the build configuration and either
removing the tokens from the log messages or replacing them with masked versions
to prevent sensitive data exposure.


return true
let userState = UserState(rawValue: loginResponse.userState)
return userState
} catch {
BitnagilLogger.log(logType: .error, message: "\(error.localizedDescription)")
return false
return nil
}
Comment on lines +25 to 45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

오류 흐름과 반환값 의미를 분리하면 호출부가 더 안전해집니다

현재 실패 시 nil 을 반환하고 동일 타입으로 게스트·미로그인도 표현합니다.
reissueToken() async throws -> UserState? 로 바꾸고,

  • 성공 + 유저/게스트 ⇒ .user / .guest
  • 실패 ⇒ throw Error
    형태로 분리하면 SceneDelegate 등에서 switch 로직이 간결-명확해집니다.
🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Repository/UserDataRepository.swift around lines
25 to 45, change the function signature of reissueToken() to async throws ->
UserState? to separate error handling from the return value. Modify the
implementation so that on failure it throws the error instead of returning nil,
and on success it returns .user or .guest accordingly. This will make the
calling code safer and clearer by handling errors via throws and user states via
the return value.

}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ public protocol UserDataRepositoryProtocol {
func loadNickname() async throws -> String

/// 토큰 재발급을 진행합니다.
func reissueToken() async -> Bool
func reissueToken() async -> UserState?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

nil 반환으로는 오류·게스트 구분이 불명확합니다

UserState? 옵셔널만으로 네트워크 오류, 토큰 만료, 디코딩 실패 등과 게스트 상태를 모두 나타내기에는 표현력이 부족합니다.
throws 또는 Result<UserState, Error> 패턴으로 오류를 분리해 주면 호출부가 의도‧실패 원인을 명확히 처리할 수 있습니다.

🤖 Prompt for AI Agents
In Projects/Domain/Sources/Protocol/Repository/UserDataRepositoryProtocol.swift
at line 15, the method reissueToken() returns an optional UserState?, which does
not clearly distinguish between errors, guest states, or other failure reasons.
Change the method signature to either throw errors or return a Result<UserState,
Error> to separate success and failure cases explicitly, allowing callers to
handle different error types and guest states clearly.

}
48 changes: 32 additions & 16 deletions Projects/Presentation/Sources/Home/View/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,15 @@ final class HomeView: BaseViewController<HomeViewModel> {
static let deleteAlertViewWidth: CGFloat = 298
static let deleteAlertViewHeight: CGFloat = 214
static let toastMessageBottomSpacing: CGFloat = 19
static let toastMessageWidth: CGFloat = 265
static let toastMessageHeight: CGFloat = 44
}

private let gradientLayer = CAGradientLayer()
private let homeLabel = UILabel()
private let informationButton = UIButton()
private let emotionOrbView = UIImageView()
private let registerEmotionButtonActionIdentifier = UIAction.Identifier("goToEmotionRegisterView")
private let registerEmotionButton = HomeRegisterEmotionButton()

private let contentView = UIView()
Expand Down Expand Up @@ -133,14 +136,10 @@ final class HomeView: BaseViewController<HomeViewModel> {
}
}, for: .touchUpInside)

registerEmotionButton.addAction(UIAction { [weak self] _ in
guard let emotionRegisterViewModel = DIContainer.shared.resolve(type: EmotionRegisterViewModel.self) else {
fatalError("emotionRegisterViewModel 의존성이 등록되지 않았습니다.")
}
let emotionRegisterView = EmotionRegisterView(viewModel: emotionRegisterViewModel)
emotionRegisterView.hidesBottomBarWhenPushed = true
self?.navigationController?.pushViewController(emotionRegisterView, animated: true)
}, for: .touchUpInside)
let registerEmotionAction = UIAction(identifier: registerEmotionButtonActionIdentifier) { [weak self] _ in
self?.goToEmotionRegisterView()
}
registerEmotionButton.addAction(registerEmotionAction, for: .touchUpInside)

contentView.backgroundColor = .white
contentView.layer.cornerRadius = Layout.contentViewCornerRadius
Expand Down Expand Up @@ -344,15 +343,16 @@ final class HomeView: BaseViewController<HomeViewModel> {
.sink { [weak self] fetchRoutineResult in
if fetchRoutineResult {
self?.viewModel.action(input: .refreshSelectedDateRoutine)
self?.hideIndicatorView()
}
self?.hideIndicatorView()
}
.store(in: &cancellables)

viewModel.output.routinesPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] routines in
self?.updateRoutineView(routines: routines)
self?.hideIndicatorView()
}
.store(in: &cancellables)

Expand Down Expand Up @@ -434,17 +434,24 @@ final class HomeView: BaseViewController<HomeViewModel> {
guard
let emotion,
let emotionOrbImageUrl = emotion.emotionImageUrl else {
let registerEmotionAction = UIAction(identifier: registerEmotionButtonActionIdentifier) { [weak self] _ in
self?.goToEmotionRegisterView()
}
registerEmotionButton.addAction(registerEmotionAction, for: .touchUpInside)
emotionOrbView.image = BitnagilGraphic.defaultEmotionGraphic
return
}
emotionOrbView.kf.setImage(with: emotionOrbImageUrl)
registerEmotionButton.isEnabled = false

toastMessageView.showToast(
withCheckImage: true,
message: "선택한 감정 구슬이 이미 반영되었어요.",
width: 265,
height: 44)
registerEmotionButton.removeAction(identifiedBy: registerEmotionButtonActionIdentifier, for: .touchUpInside)
registerEmotionButton.addAction(
UIAction { [weak self] _ in
self?.toastMessageView.showToast(
withCheckImage: true,
message: "선택한 감정 구슬이 이미 반영되었어요.",
width: Layout.toastMessageWidth,
height: Layout.toastMessageHeight)
},
for: .touchUpInside)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
Expand Down Expand Up @@ -561,6 +568,15 @@ final class HomeView: BaseViewController<HomeViewModel> {
loadingIndicatorView.stopAnimating()
contentView.isUserInteractionEnabled = true
}

private func goToEmotionRegisterView() {
guard let emotionRegisterViewModel = DIContainer.shared.resolve(type: EmotionRegisterViewModel.self) else {
fatalError("emotionRegisterViewModel 의존성이 등록되지 않았습니다.")
}
let emotionRegisterView = EmotionRegisterView(viewModel: emotionRegisterViewModel)
emotionRegisterView.hidesBottomBarWhenPushed = true
navigationController?.pushViewController(emotionRegisterView, animated: true)
}
}

// MARK: RoutineViewDelegate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,14 @@ final class ResultRecommendedRoutineView: BaseViewController<ResultRecommendedRo
}
}
.store(in: &cancellables)

viewModel.output.selectedRoutineIdPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] routineId in
guard let routineId else { return }
self?.goToRoutineCreationView(routineId: routineId)
}
.store(in: &cancellables)
}

// 추천 루틴 뷰들을 업데이트합니다.
Expand Down Expand Up @@ -352,10 +360,14 @@ final class ResultRecommendedRoutineView: BaseViewController<ResultRecommendedRo
tabBarView.selectedIndex = 2
}
case .emotion:
guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self)
else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") }
let routineCreationView = RoutineCreationView(viewModel: routineCreationViewModel)
self.navigationController?.pushViewController(routineCreationView, animated: true)
viewModel.action(input: .fetchSelectedRoutineId)
}
}

private func goToRoutineCreationView(routineId: Int) {
guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self)
else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") }
let routineCreationView = RoutineCreationView(viewModel: routineCreationViewModel, recommendRoutineId: routineId)
self.navigationController?.pushViewController(routineCreationView, animated: true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,22 @@ final class ResultRecommendedRoutineViewModel: ViewModel {

enum Input {
case fetchResultRecommendedRoutines
case fetchSelectedRoutineId
case selectRecommendedRoutine(routine: RecommendedRoutine)
case registerRecommendedRoutine
}

struct Output {
let resultRecommendedRoutinesPublisher: AnyPublisher<[RecommendedRoutine], Never>
let selectedRoutineIdPublisher: AnyPublisher<Int?, Never>
let selectedRecommendedRoutinePublisher: AnyPublisher<Set<RecommendedRoutine>, Never>
let confirmButtonPublisher: AnyPublisher<Bool, Never>
let registerRoutineResultPublisher: AnyPublisher<Bool, Never>
}

private(set) var output: Output
private let resultRecommendedRoutinesSubject = CurrentValueSubject<[RecommendedRoutine], Never>([])
private let selectedRoutineIdSubject = PassthroughSubject<Int?, Never>()
private let selectedRecommendedRoutineSubject = CurrentValueSubject<Set<RecommendedRoutine>, Never>([])
private let confirmButtonSubject = PassthroughSubject<Bool, Never>()
private let registerRoutineResultSubject = PassthroughSubject<Bool, Never>()
Expand All @@ -42,6 +45,7 @@ final class ResultRecommendedRoutineViewModel: ViewModel {
self.resultRecommendedRoutineUseCase = resultRecommendedRoutineUseCase
output = Output(
resultRecommendedRoutinesPublisher: resultRecommendedRoutinesSubject.eraseToAnyPublisher(),
selectedRoutineIdPublisher: selectedRoutineIdSubject.eraseToAnyPublisher(),
selectedRecommendedRoutinePublisher: selectedRecommendedRoutineSubject.eraseToAnyPublisher(),
confirmButtonPublisher: confirmButtonSubject.eraseToAnyPublisher(),
registerRoutineResultPublisher: registerRoutineResultSubject.eraseToAnyPublisher()
Expand All @@ -53,6 +57,10 @@ final class ResultRecommendedRoutineViewModel: ViewModel {
case .fetchResultRecommendedRoutines:
fetchResultRecommendedRoutines()

case .fetchSelectedRoutineId:
let routineId = selectedRecommendedRoutineSubject.value.first?.id
selectedRoutineIdSubject.send(routineId)

case .selectRecommendedRoutine(let routine):
selectRecommendedRoutine(routine: routine)

Expand Down
2 changes: 1 addition & 1 deletion SupportingFiles/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.0.1</string>
<string>0.0.3</string>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Info.plist에 버전 하드코딩 대신 MARKETING_VERSION/CURRENT_PROJECT_VERSION 사용 권장

  • 여러 타깃/플랫폼 확장 시 버전 불일치가 생기지 않도록 CFBundleShortVersionString은 $(MARKETING_VERSION)로, CFBundleVersion은 $(CURRENT_PROJECT_VERSION)로 관리하는 것을 권장합니다.
  • TestFlight 업로드 시 CFBundleVersion은 단조 증가해야 하므로, 빌드 넘버 증가 전략(CI에서 자동 증가 등)을 함께 설정하세요.
  • 주의: 프로젝트 Build Settings에 MARKETING_VERSION/CURRENT_PROJECT_VERSION가 정의되어 있어야 합니다.

변경 제안(해당 라인 교체):

-	<string>0.0.3</string>
+	<string>$(MARKETING_VERSION)</string>

CFBundleVersion도 함께 변수화 권장(파일 내 다른 위치, 참고용 XML):

<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>

버전 일관성 검증 스크립트:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Info.plist 버전 값 점검 =="
fd -a 'Info.plist' | while read -r f; do
  echo "---- $f"
  rg -n -A 1 '<key>CFBundleShortVersionString</key>|<key>CFBundleVersion</key>' "$f" || true
done

Length of output: 484


Info.plist 버전 하드코딩 제거 및 변수화 적용 필요

  • SupportingFiles/Info.plist 내에 버전이 하드코딩(0.0.3, 1)되어 있습니다.
    • CFBundleShortVersionString$(MARKETING_VERSION)으로,
    • CFBundleVersion$(CURRENT_PROJECT_VERSION)으로 관리하세요.
  • TestFlight 업로드를 위해 빌드 번호(CFBundleVersion)는 단조 증가 설정(CI 자동 증가 등)도 함께 구성해야 합니다.
  • 프로젝트 Build Settings에 MARKETING_VERSIONCURRENT_PROJECT_VERSION이 정의되어 있는지 확인하세요.

변경 제안(diff):

--- a/SupportingFiles/Info.plist
+++ b/SupportingFiles/Info.plist
@@ -17,4 +17,4 @@
 	<key>CFBundleShortVersionString</key>
-	<string>0.0.3</string>
+	<string>$(MARKETING_VERSION)</string>
@@ -30,4 +30,4 @@
 	<key>CFBundleVersion</key>
-	<string>1</string>
+	<string>$(CURRENT_PROJECT_VERSION)</string>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<string>0.0.3</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
🤖 Prompt for AI Agents
In SupportingFiles/Info.plist at line 18, the version string is hardcoded as
"0.0.3". Replace this hardcoded version with the variable reference
$(MARKETING_VERSION) for CFBundleShortVersionString and
$(CURRENT_PROJECT_VERSION) for CFBundleVersion to enable dynamic version
management. Also, ensure that in the project Build Settings, MARKETING_VERSION
and CURRENT_PROJECT_VERSION are properly defined and that the build number
(CURRENT_PROJECT_VERSION) is configured to increment automatically for
TestFlight uploads.

<key>CFBundleURLTypes</key>
<array>
<dict>
Expand Down
Loading