[Refactor-T3-182] 마이페이지, 설정 V2디자인 반영. 강제 업데이트 구현#57
Conversation
Walkthrough자산 카탈로그에 신규 체브론 아이콘을 추가하고 DesignSystem에 회전 지원 API를 도입했습니다. 버튼 셀의 활성 테마를 주황색으로 변경했습니다. Home 화면에 버전 점검/업데이트 흐름과 DI의 AppConfigRepository 주입을 추가했습니다. 마이페이지·설정의 네비게이션·레이아웃·바인딩 스레딩을 조정했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant HVC as HomeViewController
participant HVM as HomeViewModel
participant ACR as AppConfigRepository
participant Store as App Store
rect rgb(242,247,255)
note right of HVC: 뷰 로드 시 버전 점검 시작
HVC->>HVM: action(.fetchVersion)
HVM->>ACR: fetchAppStoreVersion()
ACR-->>HVM: appStoreVersion, updateURL?
alt 업데이트 필요
HVM-->>HVC: output.updateVersionPublisher(updateURL)
HVC->>HVC: 업데이트 알럿 표시(업데이트 / 취소)
alt 사용자: 업데이트
HVC->>Store: open(updateURL)
HVC->>HVC: exit(0)
else 사용자: 취소
HVC->>HVC: exit(0)
end
else 최신 상태
HVM-->>HVC: output.updateVersionPublisher(nil)
HVC->>HVC: 아무 동작 없음
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25–30 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
Projects/Presentation/Sources/Common/Component/TableViewCell/BitnagilButtonTableViewCell.swift (1)
35-58: SettingView 호출부와 시그니처 불일치로 컴파일 에러 발생현재 셀의
configure(title:buttonTitle:isButtonEnabled:)만 존재하는데, SettingView에서는cell.configure(title:)로 호출합니다(SettingView.swift Line 321). 오버로드 추가 또는 호출부 수정이 필요합니다.두 가지 중 하나로 정리하세요(오버로드 추가를 권장):
- 옵션 A: 셀에 오버로드 추가
func configure( title: String, buttonTitle: String, isButtonEnabled: Bool ) { ... } + +// 기본 상태(비활성)로 제목만 세팅하는 오버로드 +func configure(title: String) { + super.configure(title: title) + button.setTitle(nil, for: .normal) + button.isEnabled = false + button.backgroundColor = BitnagilColor.gray98 + button.setTitleColor(BitnagilColor.gray70, for: .disabled) +}
- 옵션 B: SettingView 호출부 수정(아래 해당 코멘트 참고).
Projects/Presentation/Sources/Setting/View/SettingView.swift (3)
188-205: 버전 셀 업데이트: 비가시 영역에서 반영 누락 가능
cellForRow(at:)는 가시 영역에 없으면 nil을 반환하므로 스크롤 시 최신 상태가 아니게 됩니다. 상태를 보존하고 행을 리로드하세요.간단한 보완:
- viewModel.output.versionPublisher - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] versionType in - let indexPath = IndexPath(row: InformationSection.version.rawValue, section: Section.information.rawValue) - guard - let self, - let cell = self.tableView.cellForRow(at: indexPath) as? BitnagilButtonTableViewCell - else { return } - - switch versionType { - case .needUpdate(let version): - cell.configure(title: "버전 \(version)", buttonTitle: "업데이트", isButtonEnabled: true) - case .latest(let version): - cell.configure(title: "버전 \(version)", buttonTitle: "최신", isButtonEnabled: false) - } - }) + viewModel.output.versionPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] versionType in + guard let self else { return } + let indexPath = IndexPath(row: InformationSection.version.rawValue, section: Section.information.rawValue) + if let cell = self.tableView.cellForRow(at: indexPath) as? BitnagilButtonTableViewCell { + switch versionType { + case .needUpdate(let version): + cell.configure(title: "버전 \(version)", buttonTitle: "업데이트", isButtonEnabled: true) + case .latest(let version): + cell.configure(title: "버전 \(version)", buttonTitle: "최신", isButtonEnabled: false) + } + } else { + // 비가시 상태면 행 리로드 + self.tableView.reloadRows(at: [indexPath], with: .none) + } + })추가로,
cellForRowAt에서 현재 버전 상태를 반영하도록 로컬 상태(예:versionCellState)를 두면 더 견고합니다. 필요하면 코드 제안 드리겠습니다.
320-325: 컴파일 에러:BitnagilButtonTableViewCell.configure(title:)미존재현재 시그니처와 맞지 않습니다. (본 셀에 오버로드 추가가 어렵다면) 호출부를 다음처럼 수정하세요.
- case .button(let title): + case .button(let title): guard let cell = tableView.dequeueReusableCell(withIdentifier: BitnagilButtonTableViewCell.className, for: indexPath) as? BitnagilButtonTableViewCell else { return .init() } - cell.configure(title: title) + // 초기 상태: 버튼 비활성(버전 정보 수신 시 갱신) + cell.configure(title: title, buttonTitle: "", isButtonEnabled: false) cell.delegate = self cell.selectionStyle = .none return cell대안: BitnagilButtonTableViewCell에
configure(title:)오버로드 추가(해당 파일 코멘트 참조).
206-226: 윈도우 교체 로직: delegate에서 window 추출은 안전하지 않음
UIWindowSceneDelegate로 캐스팅해window에 접근하면 타입에 따라 실패할 수 있습니다.UIWindowScene.windows에서 직접 가져오세요.- if !isAuthenticated { - guard - let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let sceneDelegate = windowScene.delegate as? UIWindowSceneDelegate, - let window = sceneDelegate.window - else { return } + if !isAuthenticated { + guard + let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first + else { return } guard let loginViewModel = DIContainer.shared.resolve(type: LoginViewModel.self) else { fatalError("loginViewModel 의존성이 등록되지 않았습니다.") } let loginView = LoginViewController(viewModel: loginViewModel) let navigationController = UINavigationController(rootViewController: loginView) window?.rootViewController = navigationController window?.makeKeyAndVisible() }
🧹 Nitpick comments (3)
Projects/Presentation/Resources/Images.xcassets/bitnagil_chevron_icon.imageset/Contents.json (1)
1-23: 템플릿 렌더링 의도 명시로 코드 단순화 제안에셋에 template-rendering-intent를 지정하면, 코드에서 매번 withRenderingMode(.alwaysTemplate) 호출을 피할 수 있습니다.
{ "images" : [ ... ], + "properties" : { + "template-rendering-intent" : "template" + }, "info" : { "author" : "xcode", "version" : 1 } }Projects/Presentation/Sources/Common/Component/TableViewCell/BitnagilChevronTableViewCell.swift (1)
30-34: 사소한 UI 제안: 컨텐트 모드 명시16x16 고정 사이즈이므로 크롭 방지를 위해
contentMode = .scaleAspectFit을 명시하면 안전합니다.chevronImageView.tintColor = .black chevronImageView.image = BitnagilIcon .bitnagilChevronIcon(direction: .right)? .withRenderingMode(.alwaysTemplate) + chevronImageView.contentMode = .scaleAspectFitProjects/Presentation/Sources/MyPage/View/MypageView.swift (1)
62-63: 아이콘 렌더링을 alwaysOriginal로 변경 — 다크모드 대비 확인 필요템플릿 틴트 해제를 선택했으므로, 에셋 자체가 다크/라이트 모드에 적합한 색상인지 확인하세요. 멀티컬러 또는 다크모드 전용 에셋이 없으면 대비가 낮아질 수 있습니다.
필요 시 에셋 카탈로그에 Appearance(Any, Dark) 별 이미지를 추가해 자동 전환을 지원하거나, 템플릿 유지 + 틴트 컬러를 디자인 토큰으로 관리하는 방식을 고려해주세요.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (6)
Projects/Presentation/Resources/Images.xcassets/bitnagil_chevron_icon.imageset/bitnagil_chevron_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/bitnagil_chevron_icon.imageset/bitnagil_chevron_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/bitnagil_chevron_icon.imageset/bitnagil_chevron_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/setting_icon.imageset/setting_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/setting_icon.imageset/setting_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/setting_icon.imageset/setting_icon@3x.pngis excluded by!**/*.png
📒 Files selected for processing (9)
Projects/Presentation/Resources/Images.xcassets/bitnagil_chevron_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Sources/Common/Component/TableViewCell/BitnagilButtonTableViewCell.swift(1 hunks)Projects/Presentation/Sources/Common/Component/TableViewCell/BitnagilChevronTableViewCell.swift(1 hunks)Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift(1 hunks)Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift(1 hunks)Projects/Presentation/Sources/Home/View/HomeViewController.swift(2 hunks)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift(7 hunks)Projects/Presentation/Sources/MyPage/View/MypageView.swift(3 hunks)Projects/Presentation/Sources/Setting/View/SettingView.swift(4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-16T09:09:13.869Z
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Applied to files:
Projects/Presentation/Sources/Setting/View/SettingView.swift
📚 Learning: 2025-08-01T06:38:18.687Z
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#31
File: Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift:97-103
Timestamp: 2025-08-01T06:38:18.687Z
Learning: choijungp의 HomeViewModel에서 fetchRoutines(for date: Date) 메서드는 기존 캐시 범위를 확장하는 것이 아니라, 요청된 날짜 주변의 새로운 데이터 청크를 가져오기 위해 oldestDate와 latestDate를 모두 업데이트한다. 과거 날짜 요청 시에는 해당 날짜의 1주 전~1일 전, 미래 날짜 요청 시에는 해당 날짜의 1일 후~1주 후 범위로 설정한다.
Applied to files:
Projects/Presentation/Sources/Home/View/HomeViewController.swift
🧬 Code graph analysis (6)
Projects/Presentation/Sources/Common/Component/TableViewCell/BitnagilChevronTableViewCell.swift (1)
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
bitnagilChevronIcon(19-21)
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
resolve(18-25)
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
Projects/Presentation/Sources/Common/Extension/UIImage+.swift (2)
rotate(56-67)rotate(69-71)
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (2)
Projects/Presentation/Sources/Setting/ViewModel/SettingViewModel.swift (1)
fetchVersion(139-155)Projects/DataSource/Sources/Repository/AppConfigRepository.swift (1)
fetchAppVersion(13-21)
Projects/Presentation/Sources/Setting/View/SettingView.swift (2)
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
configureCustomNavigationBar(19-29)Projects/Presentation/Sources/Setting/View/Component/SettingHeaderView.swift (1)
configure(58-66)
Projects/Presentation/Sources/Home/View/HomeViewController.swift (2)
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (1)
action(84-117)Projects/Presentation/Sources/Setting/ViewModel/SettingViewModel.swift (1)
fetchVersion(139-155)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (10)
Projects/Presentation/Resources/Images.xcassets/bitnagil_chevron_icon.imageset/Contents.json (1)
1-23: 체브론 에셋 추가 LGTM1x/2x/3x 스케일 정의와 메타데이터 구성 문제 없습니다.
Projects/Presentation/Sources/Common/Component/TableViewCell/BitnagilButtonTableViewCell.swift (1)
51-54: 활성 테마 변경(오렌지) 적용 적절UI 정책 반영으로 보이며 색상 전환 로직 문제 없습니다.
Projects/Presentation/Sources/Setting/View/SettingView.swift (1)
344-346: 정보 섹션 헤더 Divider 숨김 처리 적절V2 디자인 의도와 일치합니다.
Projects/Presentation/Sources/Common/Component/TableViewCell/BitnagilChevronTableViewCell.swift (1)
30-34: 새 아이콘 API로 교체 LGTM
BitnagilIcon.bitnagilChevronIcon(direction:)적용 및 템플릿 렌더링 유지 모두 적절합니다.Projects/Presentation/Sources/MyPage/View/MypageView.swift (2)
52-53: 네비게이션 바 표시 전환 적절SettingView에서 커스텀 네비게이션 바를 숨기는 흐름과 충돌 없이 MyPage에서는 시스템 바 표시로 복귀됩니다.
110-125: 메인 스레드 디스패치 추가 LGTMUI 바인딩에
receive(on: .main)을 추가한 점 적절합니다.Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (2)
35-40: HomeViewModel 호출부 점검 완료
HomeViewModel(호출은 PresentationDependencyAssembler.swift의 한 곳뿐이며, 다른 수동 생성 호출부는 존재하지 않습니다.
31-33: AppConfigRepositoryProtocol DI 등록 확인 — 누락 아님
DataSourceDependencyAssembler에서DIContainer.shared.register(type: AppConfigRepositoryProtocol.self)가 이미 호출되고 있어 PresentationDependencyAssembler의resolve가 정상 동작합니다.Likely an incorrect or invalid review comment.
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (2)
23-24: 승인:.fetchVersion액션 트리거 배선 확인
SettingView.swift(134)와 HomeViewController.swift(106)에서.action(input: .fetchVersion)호출이 확인되었습니다.
35-36: DI 주입 검증 필요: Assembler에서appConfigRepository등록 보장
updateVersionPublisher/updateVersionSubject추가는 적절합니다.appConfigRepository를 non-optional로 DI하는 건 좋으나, Assembler 조립 단계에서 반드시 등록되어야 런타임 크래시를 방지할 수 있습니다.
| static let bitnagilChevronIcon = UIImage(named: "chevron_icon", in: bundle, with: nil) | ||
| static func bitnagilChevronIcon(direction: Direction) -> UIImage? { | ||
| return BitnagilIcon.bitnagilChevronIcon?.rotate(degrees: direction.rotation)?.withRenderingMode(.alwaysTemplate) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
새 체브론 API가 기존 chevron API와 중복이고, 에셋명이 불일치합니다.
- 이미 47-50라인에
chevronIcon(direction:)가 동일한 역할을 합니다. 두 API가 공존하면 호출부 혼란과 유지보수 비용이 커집니다. - 신규 에셋이
bitnagil_chevron_icon으로 추가되었다면, 현재 코드의"chevron_icon"은 오타/불일치로 보입니다. 런타임에서 이미지 로드 실패 가능성이 높습니다.
아래처럼 에셋명을 바로잡고, 기존 API를 신규 API로 위임시키거나(deprecate) 하나로 정리해 주세요.
- static let bitnagilChevronIcon = UIImage(named: "chevron_icon", in: bundle, with: nil)
- static func bitnagilChevronIcon(direction: Direction) -> UIImage? {
- return BitnagilIcon.bitnagilChevronIcon?.rotate(degrees: direction.rotation)?.withRenderingMode(.alwaysTemplate)
- }
+ static let bitnagilChevronIcon = UIImage(named: "bitnagil_chevron_icon", in: bundle, with: nil)
+ static func bitnagilChevronIcon(direction: Direction) -> UIImage? {
+ BitnagilIcon.bitnagilChevronIcon?
+ .rotate(degrees: direction.rotation)?
+ .withRenderingMode(.alwaysTemplate)
+ }추가 제안(선택): 파일 내 기존 API를 신규 API로 위임하여 중복 제거
- static let chevronIcon = UIImage(named: "chevron_icon", in: bundle, with: nil)
- static func chevronIcon(direction: Direction) -> UIImage? {
- return BitnagilIcon.chevronIcon?.rotate(degrees: direction.rotation)?.withRenderingMode(.alwaysTemplate)
- }
+ @available(*, deprecated, message: "Use bitnagilChevronIcon(direction:) instead.")
+ static let chevronIcon = UIImage(named: "chevron_icon", in: bundle, with: nil)
+ @available(*, deprecated, message: "Use bitnagilChevronIcon(direction:) instead.")
+ static func chevronIcon(direction: Direction) -> UIImage? {
+ BitnagilIcon.bitnagilChevronIcon(direction: direction)
+ }참고: 본 파일 하단 Direction.rotation 계산 프로퍼티에 return 키워드가 없어 컴파일 오류가 발생합니다. 아래처럼 수정해 주세요(선택 코드 블록).
var rotation: Float {
switch self {
case .up: return 90
case .down: return -90
case .left: return 0
case .right: return 180
}
}🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift around
lines 18-21, the new bitnagilChevron API duplicates the existing
chevronIcon(direction:) and references the wrong asset name ("chevron_icon" vs.
the actual "bitnagil_chevron_icon"), causing runtime image load failures and API
confusion; fix by updating the UIImage(named:) asset name to
"bitnagil_chevron_icon", then remove the duplicate implementation by making the
older or newer API delegate to the canonical method (or mark the duplicate
deprecated) so only one implementation loads, and finally correct the
Direction.rotation computed property by adding explicit return statements for
each case to fix the compile error.
| super.viewDidLoad() | ||
| showIndicatorView() | ||
| viewModel.action(input: .loadNickname) | ||
| viewModel.action(input: .fetchVersion) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
버전 체크 트리거 위치 재고 필요(백그라운드 복귀 시 재검 없음)
viewDidLoad 한 번만 호출되므로, 사용자가 App Store 다녀온 뒤 앱으로 복귀해도 재검이 실행되지 않습니다. viewWillAppear 추가 트리거 또는 willEnterForeground 노티 관찰을 권장합니다.
예시(선택, 변경은 파일 범위 밖):
private var foregroundObserver: Any?
override func viewDidLoad() {
super.viewDidLoad()
foregroundObserver = NotificationCenter.default.addObserver(
forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main
) { [weak self] _ in
self?.viewModel.action(input: .fetchVersion)
}
}
deinit {
if let obs = foregroundObserver { NotificationCenter.default.removeObserver(obs) }
}🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/View/HomeViewController.swift around line
106, the version check is triggered only in viewDidLoad (viewModel.action(input:
.fetchVersion)), so returning from the App Store or background won’t re-run it;
modify the controller to also trigger fetchVersion on viewWillAppear(_:) or
observe UIApplication.willEnterForegroundNotification and call
viewModel.action(input: .fetchVersion) from the observer using [weak self], and
ensure you remove the observer in deinit (or use the new NotificationCenter
token removal) to avoid leaks.
| viewModel.output.updateVersionPublisher | ||
| .receive(on: DispatchQueue.main) | ||
| .sink { [weak self] updateURL in | ||
| guard let updateURL else { return } | ||
|
|
||
| let alert = UIAlertController( | ||
| title: "업데이트가 필요합니다", | ||
| message: "원활한 이용을 위해, 빛나길을 업데이트 해주세요!", | ||
| preferredStyle: .alert | ||
| ) | ||
|
|
||
| let cancel = UIAlertAction( | ||
| title: "취소", | ||
| style: .default, | ||
| handler: { _ in exit(0) }) | ||
| let update = UIAlertAction( | ||
| title: "업데이트", | ||
| style: .default, | ||
| handler: { _ in | ||
| UIApplication.shared.open(updateURL, options: [:], completionHandler: { _ in exit(0) }) | ||
| }) | ||
|
|
||
| alert.addAction(cancel) | ||
| alert.addAction(update) | ||
| alert.preferredAction = update | ||
| self?.present(alert, animated: true) | ||
| } | ||
| .store(in: &cancellables) | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
exit(0) 사용은 앱스토어 심사 리젝 및 UX 저하 가능성이 큽니다. 강제 업데이트는 ‘닫기 불가’ 알럿으로 대체하세요.
iOS에서 임의 종료는 가이드라인 위반 소지가 큽니다. ‘취소’ 없이 업데이트만 노출해 화면을 벗어날 수 없게 하고, 백그라운드 전환 시에도 알럿이 유지되도록 하는 방식이 일반적입니다. 아래와 같이 수정해 주세요.
- let cancel = UIAlertAction(
- title: "취소",
- style: .default,
- handler: { _ in exit(0) })
let update = UIAlertAction(
title: "업데이트",
style: .default,
- handler: { _ in
- UIApplication.shared.open(updateURL, options: [:], completionHandler: { _ in exit(0) })
- })
+ handler: { _ in
+ if UIApplication.shared.canOpenURL(updateURL) {
+ UIApplication.shared.open(updateURL, options: [:], completionHandler: nil)
+ }
+ })
- alert.addAction(cancel)
alert.addAction(update)
alert.preferredAction = update
self?.present(alert, animated: true)추가 권장:
- 알럿 외부 닫힘 방지를 위해 ‘취소’ 액션 제거(현재처럼
.alert는 바깥 탭으로 닫히지 않음). - 앱 전역(예: 루트 코디네이터/인트로 흐름)에서 업데이트 게이트를 관리해 화면 전환/탭 이동도 차단.
🏁 Script executed:
#!/bin/bash
# 위험한 프로세스 종료 사용 위치 전수 조사
rg -nP '\bexit\s*\(\s*0\s*\)' -C3Length of output: 1352
exit(0) 호출 제거 및 강제 업데이트 불가 닫힘(alert)으로 변경
iOS 임의 종료는 심사·UX 문제를 일으키므로, 취소 액션·completionHandler 내 exit(0) 호출을 제거하고 업데이트만 가능한 .alert로 구현하세요.
- 앱 전역(루트 코디네이터)에서 업데이트 게이트를 관리해 화면 전환·백그라운드 전환 시에도 알럿이 유지되도록 차단
- let cancel = UIAlertAction(
- title: "취소",
- style: .default,
- handler: { _ in exit(0) })
let update = UIAlertAction(
title: "업데이트",
style: .default,
- handler: { _ in
- UIApplication.shared.open(updateURL, options: [:], completionHandler: { _ in exit(0) })
- })
+ handler: { _ in
+ if UIApplication.shared.canOpenURL(updateURL) {
+ UIApplication.shared.open(updateURL, options: [:], completionHandler: nil)
+ }
+ })
- alert.addAction(cancel)
alert.addAction(update)
alert.preferredAction = update📝 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.
| viewModel.output.updateVersionPublisher | |
| .receive(on: DispatchQueue.main) | |
| .sink { [weak self] updateURL in | |
| guard let updateURL else { return } | |
| let alert = UIAlertController( | |
| title: "업데이트가 필요합니다", | |
| message: "원활한 이용을 위해, 빛나길을 업데이트 해주세요!", | |
| preferredStyle: .alert | |
| ) | |
| let cancel = UIAlertAction( | |
| title: "취소", | |
| style: .default, | |
| handler: { _ in exit(0) }) | |
| let update = UIAlertAction( | |
| title: "업데이트", | |
| style: .default, | |
| handler: { _ in | |
| UIApplication.shared.open(updateURL, options: [:], completionHandler: { _ in exit(0) }) | |
| }) | |
| alert.addAction(cancel) | |
| alert.addAction(update) | |
| alert.preferredAction = update | |
| self?.present(alert, animated: true) | |
| } | |
| .store(in: &cancellables) | |
| } | |
| viewModel.output.updateVersionPublisher | |
| .receive(on: DispatchQueue.main) | |
| .sink { [weak self] updateURL in | |
| guard let updateURL else { return } | |
| let alert = UIAlertController( | |
| title: "업데이트가 필요합니다", | |
| message: "원활한 이용을 위해, 빛나길을 업데이트 해주세요!", | |
| preferredStyle: .alert | |
| ) | |
| let update = UIAlertAction( | |
| title: "업데이트", | |
| style: .default, | |
| handler: { _ in | |
| if UIApplication.shared.canOpenURL(updateURL) { | |
| UIApplication.shared.open(updateURL, options: [:], completionHandler: nil) | |
| } | |
| }) | |
| alert.addAction(update) | |
| alert.preferredAction = update | |
| self?.present(alert, animated: true) | |
| } | |
| .store(in: &cancellables) | |
| } |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/View/HomeViewController.swift around lines
466 to 494, remove all exit(0) calls and make the update alert a forced-update
modal by eliminating the "취소" action (do not add exit in any handlers), keep
only the "업데이트" action which opens the URL without calling exit, and present the
alert from the app's root coordinator (or root view controller) instead of self
so the alert remains across screen/background transitions; ensure the alert
style remains .alert and do not add a cancel/dismiss action so users can only
tap "업데이트".
| private func checkVersion() { | ||
| let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String | ||
| let major = currentVersion?.split(separator: ".").first | ||
|
|
||
| Task { | ||
| do { | ||
| let appStoreAppVersion = try await appConfigRepository.fetchAppVersion() | ||
| let appStoreMajor = appStoreAppVersion?.split(separator: ".").first | ||
|
|
||
| if major != appStoreMajor { | ||
| let url = URL(string: "itms-apps://itunes.apple.com/app/id6749437799") | ||
| updateVersionSubject.send(url) | ||
| } else { | ||
| updateVersionSubject.send(nil) | ||
| } | ||
|
|
||
| } catch { | ||
|
|
||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
버전 비교 로직의 nil/파싱 예외 처리 보강 및 실패 시 안전 동작 필요
현재는 옵셔널 비교와 에러 삼키기 때문에, 일시적 네트워크/파싱 실패에도 ‘강제 업데이트’로 오인될 위험이 있습니다. 실패 시에는 업데이트 미필요로 처리하고, 비교값은 확정적으로 언랩해 주세요.
- private func checkVersion() {
- let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
- let major = currentVersion?.split(separator: ".").first
-
- Task {
- do {
- let appStoreAppVersion = try await appConfigRepository.fetchAppVersion()
- let appStoreMajor = appStoreAppVersion?.split(separator: ".").first
-
- if major != appStoreMajor {
- let url = URL(string: "itms-apps://itunes.apple.com/app/id6749437799")
- updateVersionSubject.send(url)
- } else {
- updateVersionSubject.send(nil)
- }
-
- } catch {
-
- }
- }
- }
+ private func checkVersion() {
+ guard
+ let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
+ let currentMajor = currentVersion.split(separator: ".").first
+ else {
+ updateVersionSubject.send(nil)
+ return
+ }
+
+ Task {
+ do {
+ guard
+ let appStoreVersion = try await appConfigRepository.fetchAppVersion(),
+ let appStoreMajor = appStoreVersion.split(separator: ".").first
+ else {
+ updateVersionSubject.send(nil)
+ return
+ }
+
+ if currentMajor != appStoreMajor {
+ let url = URL(string: "itms-apps://itunes.apple.com/app/id6749437799")
+ updateVersionSubject.send(url)
+ } else {
+ updateVersionSubject.send(nil)
+ }
+ } catch {
+ updateVersionSubject.send(nil)
+ }
+ }
+ }보완 포인트:
- 메이저 비교 기준은 여기서는 “상이하면 강제 업데이트”, SettingViewModel은 “버전 전체 불일치”로 보입니다. 사용자 경험 일관성을 위해 기준 통합을 검토해 주세요.
🏁 Script executed:
#!/bin/bash
# SettingViewModel의 버전 비교 기준 확인
rg -nP 'SettingViewModel' -n --type swift -C3
rg -nP 'fetchVersion\(\)|versionSubject|needUpdate|latest' -n --type swift -C3Length of output: 13336
버전 비교 로직 nil/파싱 예외 처리 강화 및 비교 기준 통합 검토
checkVersion()에서currentVersion/appStoreVersion을guard로 확실히 언랩하고, 실패 또는 에러 시updateVersionSubject.send(nil)처리- HomeViewModel(메이저 버전)과 SettingViewModel(전체 버전) 비교 기준 불일치하므로 UX 일관성에 맞춰 기준 통합 검토
private func checkVersion() {
- let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
- let major = currentVersion?.split(separator: ".").first
+ guard
+ let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
+ let currentMajor = currentVersion.split(separator: ".").first
+ else {
+ updateVersionSubject.send(nil)
+ return
+ }
Task {
do {
- let appStoreAppVersion = try await appConfigRepository.fetchAppVersion()
- let appStoreMajor = appStoreAppVersion?.split(separator: ".").first
+ guard
+ let appStoreVersion = try await appConfigRepository.fetchAppVersion(),
+ let appStoreMajor = appStoreVersion.split(separator: ".").first
+ else {
+ updateVersionSubject.send(nil)
+ return
+ }
if currentMajor != appStoreMajor {
let url = URL(string: "itms-apps://itunes.apple.com/app/id6749437799")
updateVersionSubject.send(url)
} else {
updateVersionSubject.send(nil)
}
} catch {
-
+ updateVersionSubject.send(nil)
}
}
}📝 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.
| private func checkVersion() { | |
| let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String | |
| let major = currentVersion?.split(separator: ".").first | |
| Task { | |
| do { | |
| let appStoreAppVersion = try await appConfigRepository.fetchAppVersion() | |
| let appStoreMajor = appStoreAppVersion?.split(separator: ".").first | |
| if major != appStoreMajor { | |
| let url = URL(string: "itms-apps://itunes.apple.com/app/id6749437799") | |
| updateVersionSubject.send(url) | |
| } else { | |
| updateVersionSubject.send(nil) | |
| } | |
| } catch { | |
| } | |
| } | |
| } | |
| private func checkVersion() { | |
| guard | |
| let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, | |
| let currentMajor = currentVersion.split(separator: ".").first | |
| else { | |
| updateVersionSubject.send(nil) | |
| return | |
| } | |
| Task { | |
| do { | |
| guard | |
| let appStoreVersion = try await appConfigRepository.fetchAppVersion(), | |
| let appStoreMajor = appStoreVersion.split(separator: ".").first | |
| else { | |
| updateVersionSubject.send(nil) | |
| return | |
| } | |
| if currentMajor != appStoreMajor { | |
| let url = URL(string: "itms-apps://itunes.apple.com/app/id6749437799") | |
| updateVersionSubject.send(url) | |
| } else { | |
| updateVersionSubject.send(nil) | |
| } | |
| } catch { | |
| updateVersionSubject.send(nil) | |
| } | |
| } | |
| } |
| override func viewWillAppear(_ animated: Bool) { | ||
| super.viewWillAppear(animated) | ||
| // configureNavigationBar(navigationStyle: .withBackButton(title: "설정")) | ||
| navigationController?.navigationBar.isHidden = true | ||
| configureCustomNavigationBar(navigationBarStyle: .withBackButton(title: "설정")) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
viewWillAppear에서 커스텀 네비게이션 바를 매번 추가 — 중복 추가 위험
configureCustomNavigationBar(...)가 매 진입마다 새로운 뷰를 붙이면 중복/레이아웃 누적 문제가 발생합니다. 1회만 추가하도록 가드하세요.
final class SettingView: BaseViewController<SettingViewModel> {
+ private var didAddCustomNavigationBar = Bool(false)
...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.isHidden = true
- configureCustomNavigationBar(navigationBarStyle: .withBackButton(title: "설정"))
+ if !didAddCustomNavigationBar {
+ configureCustomNavigationBar(navigationBarStyle: .withBackButton(title: "설정"))
+ didAddCustomNavigationBar = true
+ }
}📝 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.
| override func viewWillAppear(_ animated: Bool) { | |
| super.viewWillAppear(animated) | |
| // configureNavigationBar(navigationStyle: .withBackButton(title: "설정")) | |
| navigationController?.navigationBar.isHidden = true | |
| configureCustomNavigationBar(navigationBarStyle: .withBackButton(title: "설정")) | |
| } | |
| final class SettingView: BaseViewController<SettingViewModel> { | |
| private var didAddCustomNavigationBar = Bool(false) | |
| override func viewWillAppear(_ animated: Bool) { | |
| super.viewWillAppear(animated) | |
| navigationController?.navigationBar.isHidden = true | |
| if !didAddCustomNavigationBar { | |
| configureCustomNavigationBar(navigationBarStyle: .withBackButton(title: "설정")) | |
| didAddCustomNavigationBar = true | |
| } | |
| } | |
| // ... rest of the class ... | |
| } |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Setting/View/SettingView.swift around lines
111–115, viewWillAppear currently calls configureCustomNavigationBar every time
which can attach the custom nav bar repeatedly; change to only add it once by
guarding (either move the call to viewDidLoad or add a boolean flag like
hasConfiguredCustomNavBar that you set after first configuration, or check for
an existing custom nav bar subview/tag before calling
configureCustomNavigationBar), keep the
navigationController?.navigationBar.isHidden = true behavior but ensure
configureCustomNavigationBar is skipped when already added.
| UIApplication.shared.open(updateURL, options: [:], completionHandler: { _ in exit(0) }) | ||
| }) |
There was a problem hiding this comment.
딩이 강종이 심사 리젝 사유라고 해가주구 생각해봤는데욤 ... 그냥 검정 화면 혹은 빈 화면을 띄우는건 어떤지 ??
There was a problem hiding this comment.
아하 앱에 다시 돌아왔을때 빈 화면만 보여주자는 말씀이신가요??
There was a problem hiding this comment.
네네네 !!!!!! 만약 리젝 된다면 .. ㅎㅎㅎㅎㅎ
There was a problem hiding this comment.
조아요 꼼수더라도 일단 우회 고~!
| let alert = UIAlertController( | ||
| title: "업데이트가 필요합니다", | ||
| message: "원활한 이용을 위해, 빛나길을 업데이트 해주세요!", | ||
| preferredStyle: .alert | ||
| ) |
There was a problem hiding this comment.
| let alert = UIAlertController( | |
| title: "업데이트가 필요합니다", | |
| message: "원활한 이용을 위해, 빛나길을 업데이트 해주세요!", | |
| preferredStyle: .alert | |
| ) | |
| let alert = UIAlertController( | |
| title: "업데이트가 필요합니다", | |
| message: "원활한 이용을 위해, 빛나길을 업데이트 해주세요!", | |
| preferredStyle: .alert) |
요것이 컨벤션에 맞는 것 같습니두아 !!!!
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
Projects/Presentation/Sources/Common/Component/TableViewCell/BitnagilChevronTableViewCell.swift (3)
31-35: withRenderingMode 중복 제거 + RTL 대응 + 다크 모드 컬러 제안
- BitnagilIcon.bitnagilChevronIcon(direction:)에서 이미
.withRenderingMode(.alwaysTemplate)를 적용하고 있어 여기서의 재호출은 중복입니다.- 방향을
.right로 고정하면 RTL 환경(아랍어 등)에서 시각적으로 부자연스럽습니다.semanticContentAttribute기반으로 방향을 유추해 주세요.- 틴트 컬러를
.black로 고정하면 다크 모드에서 대비/일관성이 떨어질 수 있습니다..label또는 DesignSystem 컬러 토큰 사용을 권장합니다.- 아이콘 왜곡 방지를 위해
contentMode = .scaleAspectFit추가 추천.- chevronImageView.tintColor = .black - chevronImageView.image = BitnagilIcon - .bitnagilChevronIcon(direction: .right)? - .withRenderingMode(.alwaysTemplate) + chevronImageView.tintColor = .label // 또는 DesignSystem 컬러 토큰 + let layoutDir = UIView.userInterfaceLayoutDirection(for: contentView.semanticContentAttribute) + let chevronDirection: BitnagilIcon.Direction = (layoutDir == .rightToLeft) ? .left : .right + chevronImageView.image = BitnagilIcon + .bitnagilChevronIcon(direction: chevronDirection) + chevronImageView.contentMode = .scaleAspectFit
13-15: 사이즈 상수 통합 및 스페이싱 토큰화 검토
- width/height를 별도 상수로 둘 필요가 크지 않습니다.
CGSize하나로 통합하면 가독성이 좋아지고 제약 설정도 간결해집니다.- trailing 스페이싱(20pt)은 다른 셀/컴포넌트와의 일관성 유지가 중요합니다. DesignSystem의 spacing 토큰(예:
Spacing.lg)으로 관리하는 방안을 고려해 주세요.- static let chevronImageViewTrailingSpacing: CGFloat = 20 - static let chevronImageWidth: CGFloat = 7 - static let chevronImageHeight: CGFloat = 11 + static let chevronImageViewTrailingSpacing: CGFloat = 20 // TODO: DS 토큰으로 교체 검토 + static let chevronImageSize = CGSize(width: 7, height: 11)
45-46: SnapKit 제약 간결화 (size(equalTo:))위 사이즈 상수 통합 제안과 함께 사용하면 제약을 더 간단히 표현할 수 있습니다.
- make.width.equalTo(Layout.chevronImageWidth) - make.height.equalTo(Layout.chevronImageHeight) + make.size.equalTo(Layout.chevronImageSize)
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
Projects/Presentation/Sources/Common/Component/TableViewCell/BitnagilChevronTableViewCell.swift(3 hunks)Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift(1 hunks)Projects/Presentation/Sources/Home/View/HomeViewController.swift(2 hunks)Projects/Presentation/Sources/Setting/View/SettingView.swift(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- Projects/Presentation/Sources/Home/View/HomeViewController.swift
- Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift
- Projects/Presentation/Sources/Setting/View/SettingView.swift
🧰 Additional context used
🧬 Code graph analysis (1)
Projects/Presentation/Sources/Common/Component/TableViewCell/BitnagilChevronTableViewCell.swift (1)
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
bitnagilChevronIcon(19-21)

🌁 Background
📱 Screenshot
| iPhone SE3 | iPhone 13 mini | iPhone 16 Pro |
마이페이지
설정
👩💻 Contents
✅ Testing
테스트 목적과 상황
시나리오 진행에 필요한 값
시나리오 진행에 필요한 조건
시나리오 완료 시 보장하는 결과
📝 Review Note
📣 Related Issue
📬 Reference
Summary by CodeRabbit
신규 기능
스타일