-
Notifications
You must be signed in to change notification settings - Fork 1
[Feat] 설정 화면 로직 구현 #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // | ||
| // AppVersionDTO.swift | ||
| // DataSource | ||
| // | ||
| // Created by 이동현 on 8/5/25. | ||
| // | ||
|
|
||
| struct AppVersionDTO: Codable { | ||
| let resultCount: Int | ||
| let results: [AppStoreResultDTO] | ||
| } | ||
|
|
||
| struct AppStoreResultDTO: Codable { | ||
| let version: String | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,22 @@ | ||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||
| // AppConfigRepository.swift | ||||||||||||||||||||||||||||||||
| // DataSource | ||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||
| // Created by 이동현 on 8/5/25. | ||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import Domain | ||||||||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| final class AppConfigRepository: AppConfigRepositoryProtocol { | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| func fetchAppVersion() async throws -> String? { | ||||||||||||||||||||||||||||||||
| guard let bundleId = Bundle.main.bundleIdentifier else { return nil } | ||||||||||||||||||||||||||||||||
| let urlString = "https://itunes.apple.com/lookup?bundleId=\(bundleId)&country=KR" | ||||||||||||||||||||||||||||||||
| guard let url = URL(string: urlString) else { return nil } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let (data, _) = try await URLSession.shared.data(from: url) | ||||||||||||||||||||||||||||||||
| let decoded = try JSONDecoder().decode(AppVersionDTO.self, from: data) | ||||||||||||||||||||||||||||||||
| return decoded.results.first?.version | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+18
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HTTP 응답 상태 코드 검증이 필요합니다. 네트워크 요청 후 HTTP 상태 코드를 확인하지 않고 있어, 서버 오류나 잘못된 응답을 처리하지 못할 수 있습니다. -let (data, _) = try await URLSession.shared.data(from: url)
+let (data, response) = try await URLSession.shared.data(from: url)
+guard let httpResponse = response as? HTTPURLResponse,
+ (200...299).contains(httpResponse.statusCode) else {
+ throw NSError(domain: "AppConfigRepository",
+ code: (response as? HTTPURLResponse)?.statusCode ?? -1,
+ userInfo: [NSLocalizedDescriptionKey: "Invalid response from App Store"])
+}
let decoded = try JSONDecoder().decode(AppVersionDTO.self, from: data)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // | ||
| // AppConfigRepositoryProtocol.swift | ||
| // Domain | ||
| // | ||
| // Created by 이동현 on 8/5/25. | ||
| // | ||
|
|
||
| public protocol AppConfigRepositoryProtocol { | ||
| func fetchAppVersion() async throws -> String? | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "images" : [ | ||
| { | ||
| "filename" : "profile_graphic.png", | ||
| "idiom" : "universal", | ||
| "scale" : "1x" | ||
| }, | ||
| { | ||
| "filename" : "profile_graphic@2x.png", | ||
| "idiom" : "universal", | ||
| "scale" : "2x" | ||
| }, | ||
| { | ||
| "filename" : "profile_graphic@3x.png", | ||
| "idiom" : "universal", | ||
| "scale" : "3x" | ||
| } | ||
| ], | ||
| "info" : { | ||
| "author" : "xcode", | ||
| "version" : 1 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -6,7 +6,9 @@ | |||||
| // | ||||||
|
|
||||||
| import Combine | ||||||
| import Domain | ||||||
| import SafariServices | ||||||
| import Shared | ||||||
| import SnapKit | ||||||
| import UIKit | ||||||
|
|
||||||
|
|
@@ -25,14 +27,14 @@ final class SettingView: BaseViewController<SettingViewModel> { | |||||
| } | ||||||
|
|
||||||
| private enum Section: Int, CaseIterable { | ||||||
| case notification | ||||||
| // case notification | ||||||
| case information | ||||||
| case account | ||||||
|
|
||||||
| var title: String { | ||||||
| switch self { | ||||||
| case .notification: | ||||||
| return "알림" | ||||||
| // case .notification: | ||||||
| // return "알림" | ||||||
| case .information: | ||||||
| return "정보" | ||||||
| case .account: | ||||||
|
|
@@ -114,6 +116,11 @@ final class SettingView: BaseViewController<SettingViewModel> { | |||||
| override func configureAttribute() { | ||||||
| view.backgroundColor = .white | ||||||
|
|
||||||
| guard | ||||||
| let authRepository = DIContainer.shared.resolve(type: AuthRepositoryProtocol.self), | ||||||
| let appConfigRepository = DIContainer.shared.resolve(type: AppConfigRepositoryProtocol.self) | ||||||
| else { fatalError("authRepository, appConfigRepository 의존성이 등록되지 않았습니다.") } | ||||||
|
|
||||||
| tableView.delegate = self | ||||||
| tableView.dataSource = self | ||||||
| tableView.separatorStyle = .none | ||||||
|
|
@@ -122,6 +129,8 @@ final class SettingView: BaseViewController<SettingViewModel> { | |||||
| tableView.register(BitnagilButtonTableViewCell.self, forCellReuseIdentifier: BitnagilButtonTableViewCell.className) | ||||||
| tableView.register(BitnagilChevronTableViewCell.self, forCellReuseIdentifier: BitnagilChevronTableViewCell.className) | ||||||
| tableView.register(SettingHeaderView.self, forHeaderFooterViewReuseIdentifier: SettingHeaderView.className) | ||||||
| viewModel.configure(authRepository: authRepository, appConfigRepository: appConfigRepository) | ||||||
| viewModel.action(input: .fetchVersion) | ||||||
| } | ||||||
|
|
||||||
| override func configureLayout() { | ||||||
|
|
@@ -133,39 +142,42 @@ final class SettingView: BaseViewController<SettingViewModel> { | |||||
| } | ||||||
|
|
||||||
| override func bind() { | ||||||
| viewModel.output.generalNotificationEnabled | ||||||
| .receive(on: DispatchQueue.main) | ||||||
| .sink(receiveValue: { [weak self] isEnabled in | ||||||
| let indexPath = IndexPath(row: NotificationSection.general.rawValue, section: Section.notification.rawValue) | ||||||
| guard | ||||||
| let self, | ||||||
| let cell = self.tableView.cellForRow(at: indexPath) as? BitnagilToggleTableViewCell | ||||||
| else { return } | ||||||
|
|
||||||
| cell.configureToggleState(isOn: isEnabled) | ||||||
| }) | ||||||
| .store(in: &cancellables) | ||||||
|
|
||||||
| viewModel.output.pushNotificationEnabled | ||||||
| .receive(on: DispatchQueue.main) | ||||||
| .sink(receiveValue: { [weak self] isEnabled in | ||||||
| let indexPath = IndexPath(row: NotificationSection.push.rawValue, section: Section.notification.rawValue) | ||||||
| guard | ||||||
| let self, | ||||||
| let cell = self.tableView.cellForRow(at: indexPath) as? BitnagilToggleTableViewCell | ||||||
| else { return } | ||||||
|
|
||||||
| cell.configureToggleState(isOn: isEnabled) | ||||||
| }) | ||||||
| .store(in: &cancellables) | ||||||
| // viewModel.output.generalNotificationEnabled | ||||||
| // .receive(on: DispatchQueue.main) | ||||||
| // .sink(receiveValue: { [weak self] isEnabled in | ||||||
| // let indexPath = IndexPath(row: NotificationSection.general.rawValue, section: Section.notification.rawValue) | ||||||
| // guard | ||||||
| // let self, | ||||||
| // let cell = self.tableView.cellForRow(at: indexPath) as? BitnagilToggleTableViewCell | ||||||
| // else { return } | ||||||
| // | ||||||
| // cell.configureToggleState(isOn: isEnabled) | ||||||
| // }) | ||||||
| // .store(in: &cancellables) | ||||||
| // | ||||||
| // viewModel.output.pushNotificationEnabled | ||||||
| // .receive(on: DispatchQueue.main) | ||||||
| // .sink(receiveValue: { [weak self] isEnabled in | ||||||
| // let indexPath = IndexPath(row: NotificationSection.push.rawValue, section: Section.notification.rawValue) | ||||||
| // guard | ||||||
| // let self, | ||||||
| // let cell = self.tableView.cellForRow(at: indexPath) as? BitnagilToggleTableViewCell | ||||||
| // else { return } | ||||||
| // | ||||||
| // cell.configureToggleState(isOn: isEnabled) | ||||||
| // }) | ||||||
| // .store(in: &cancellables) | ||||||
|
|
||||||
| viewModel.output.urlPublisher | ||||||
| .receive(on: DispatchQueue.main) | ||||||
| .sink(receiveValue: { [weak self] url in | ||||||
| guard let url else { return } | ||||||
|
|
||||||
| let safariView = SFSafariViewController(url: url) | ||||||
| self?.present(safariView, animated: true) | ||||||
| .sink(receiveValue: { [weak self] urlType in | ||||||
| switch urlType { | ||||||
| case .internal(let url): | ||||||
| let safariView = SFSafariViewController(url: url) | ||||||
| self?.present(safariView, animated: true) | ||||||
| case .external(let url): | ||||||
| UIApplication.shared.open(url, options: [:], completionHandler: nil) | ||||||
| } | ||||||
| }) | ||||||
| .store(in: &cancellables) | ||||||
|
|
||||||
|
|
@@ -180,17 +192,28 @@ final class SettingView: BaseViewController<SettingViewModel> { | |||||
|
|
||||||
| switch versionType { | ||||||
| case .needUpdate(let version): | ||||||
| cell.configure(title: "버전\(version)", buttonTitle: "업데이트", isButtonEnabled: true) | ||||||
| cell.configure(title: "버전 \(version)", buttonTitle: "업데이트", isButtonEnabled: true) | ||||||
| case .latest(let version): | ||||||
| cell.configure(title: "버전\(version)", buttonTitle: "최신", isButtonEnabled: false) | ||||||
| cell.configure(title: "버전 \(version)", buttonTitle: "최신", isButtonEnabled: false) | ||||||
| } | ||||||
| }) | ||||||
| .store(in: &cancellables) | ||||||
|
|
||||||
| viewModel.output.isAuthenticatedPublisher | ||||||
| .receive(on: DispatchQueue.main) | ||||||
| .sink(receiveValue: { isAuthenticated in | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사용하지 않는 클로저 파라미터를 _로 대체
- .sink(receiveValue: { isAuthenticated in
+ .sink(receiveValue: { _ in📝 Committable suggestion
Suggested change
🧰 Tools🪛 SwiftLint (0.57.0)[Warning] 204-204: Unused parameter in a closure should be replaced with _ (unused_closure_parameter) 🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하 ! 아니면 logout, withdraw 시에 false를 보내니까 false 받았을 때에만 IntroView로 이동하는 것은 어떠신가요 ?? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @choijungp 좋은 제안입니다! 맞습니다. 로그아웃이나 탈퇴 시에만 IntroView로 이동해야 하므로, viewModel.output.isAuthenticatedPublisher
.receive(on: DispatchQueue.main)
.sink(receiveValue: { isAuthenticated in
guard !isAuthenticated else { return }
guard
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let sceneDelegate = windowScene.delegate as? UIWindowSceneDelegate,
let window = sceneDelegate.window
else { return }
let introView = IntroView()
let navigationController = UINavigationController(rootViewController: introView)
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
})
.store(in: &cancellables)이렇게 하면 SwiftLint 경고도 해결되고, 로직도 더 명확해집니다! 🧠 Learnings used |
||||||
| // 로그아웃 완료 후 홈 화면으로 | ||||||
| if !isAuthenticated { | ||||||
| guard | ||||||
| let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, | ||||||
| let sceneDelegate = windowScene.delegate as? UIWindowSceneDelegate, | ||||||
| let window = sceneDelegate.window | ||||||
| else { return } | ||||||
|
|
||||||
| let introView = IntroView() | ||||||
| let navigationController = UINavigationController(rootViewController: introView) | ||||||
| window?.rootViewController = navigationController | ||||||
| window?.makeKeyAndVisible() | ||||||
| } | ||||||
| }) | ||||||
| .store(in: &cancellables) | ||||||
| } | ||||||
|
|
@@ -201,8 +224,8 @@ extension SettingView: UITableViewDelegate { | |||||
| let section = Section.allCases[indexPath.section] | ||||||
|
|
||||||
| switch section { | ||||||
| case .notification: | ||||||
| return | ||||||
| // case .notification: | ||||||
| // return | ||||||
| case .information: | ||||||
| let row = InformationSection.allCases[indexPath.row] | ||||||
| switch row { | ||||||
|
|
@@ -250,8 +273,8 @@ extension SettingView: UITableViewDelegate { | |||||
| extension SettingView: UITableViewDataSource { | ||||||
| func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | ||||||
| switch Section.allCases[section] { | ||||||
| case .notification: | ||||||
| return NotificationSection.allCases.count | ||||||
| // case .notification: | ||||||
| // return NotificationSection.allCases.count | ||||||
| case .information: | ||||||
| return InformationSection.allCases.count | ||||||
| case .account: | ||||||
|
|
@@ -269,9 +292,9 @@ extension SettingView: UITableViewDataSource { | |||||
| let cellStyle: CellStyle | ||||||
|
|
||||||
| switch section { | ||||||
| case .notification: | ||||||
| let row = NotificationSection.allCases[indexPath.row] | ||||||
| cellStyle = row.cellStyle | ||||||
| // case .notification: | ||||||
| // let row = NotificationSection.allCases[indexPath.row] | ||||||
| // cellStyle = row.cellStyle | ||||||
| case .information: | ||||||
| let row = InformationSection.allCases[indexPath.row] | ||||||
| cellStyle = row.cellStyle | ||||||
|
|
@@ -306,7 +329,11 @@ extension SettingView: UITableViewDataSource { | |||||
| guard let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: SettingHeaderView.className) as? SettingHeaderView else { return nil } | ||||||
|
|
||||||
| switch section { | ||||||
| case .notification: | ||||||
| // case .notification: | ||||||
| // let view = UIView() | ||||||
| // view.backgroundColor = .white | ||||||
| // return view | ||||||
| case .information: | ||||||
| let view = UIView() | ||||||
| view.backgroundColor = .white | ||||||
| return view | ||||||
|
|
@@ -320,8 +347,8 @@ extension SettingView: UITableViewDataSource { | |||||
| let section = Section.allCases[section] | ||||||
|
|
||||||
| switch section { | ||||||
| case .notification: | ||||||
| return Layout.tableViewTopSpacing | ||||||
| // case .notification: | ||||||
| // return Layout.tableViewTopSpacing | ||||||
| default: | ||||||
| return Layout.tableViewHeaderHeight | ||||||
| } | ||||||
|
|
@@ -361,14 +388,14 @@ extension SettingView: BitnagilToggleTableViewCellDelegate { | |||||
|
|
||||||
| let section = Section.allCases[indexPath.section] | ||||||
| switch section { | ||||||
| case .notification: | ||||||
| let row = NotificationSection.allCases[indexPath.row] | ||||||
| switch row { | ||||||
| case .general: | ||||||
| viewModel.action(input: .toggleGeneralNotification) | ||||||
| case .push: | ||||||
| viewModel.action(input: .togglePushNotification) | ||||||
| } | ||||||
| // case .notification: | ||||||
| // let row = NotificationSection.allCases[indexPath.row] | ||||||
| // switch row { | ||||||
| // case .general: | ||||||
| // viewModel.action(input: .toggleGeneralNotification) | ||||||
| // case .push: | ||||||
| // viewModel.action(input: .togglePushNotification) | ||||||
| // } | ||||||
| default: | ||||||
| break | ||||||
| } | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
국가 코드를 하드코딩하지 않고 유연하게 처리하는 것을 권장합니다.
현재 "KR"로 하드코딩된 국가 코드는 향후 다른 지역 지원 시 문제가 될 수 있습니다. Bundle의 locale 정보를 사용하거나 설정 가능하도록 개선하는 것이 좋습니다.
func fetchAppVersion() async throws -> String? { guard let bundleId = Bundle.main.bundleIdentifier else { return nil } - let urlString = "https://itunes.apple.com/lookup?bundleId=\(bundleId)&country=KR" + let countryCode = Locale.current.regionCode ?? "KR" + let urlString = "https://itunes.apple.com/lookup?bundleId=\(bundleId)&country=\(countryCode)" guard let url = URL(string: urlString) else { return nil }🤖 Prompt for AI Agents