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 @@ -36,5 +36,9 @@ public struct DataSourceDependencyAssembler: DependencyAssemblerProtocol {
DIContainer.shared.register(type: RoutineRepositoryProtocol.self) { _ in
return RoutineRepository()
}

DIContainer.shared.register(type: AppConfigRepositoryProtocol.self) { _ in
return AppConfigRepository()
}
}
}
15 changes: 15 additions & 0 deletions Projects/DataSource/Sources/DTO/AppVersionDTO.swift
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
}
22 changes: 22 additions & 0 deletions Projects/DataSource/Sources/Repository/AppConfigRepository.swift
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 }
Comment on lines +15 to +16

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

국가 코드를 하드코딩하지 않고 유연하게 처리하는 것을 권장합니다.

현재 "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
In Projects/DataSource/Sources/Repository/AppConfigRepository.swift around lines
15 to 16, the country code "KR" is hardcoded in the URL string, which reduces
flexibility for supporting other regions. Modify the code to dynamically obtain
the country code from the device's locale settings or allow it to be passed as a
configurable parameter, then use that value in the URL string instead of the
fixed "KR".


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

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

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

‼️ 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
let (data, _) = try await URLSession.shared.data(from: url)
let decoded = try JSONDecoder().decode(AppVersionDTO.self, from: data)
return decoded.results.first?.version
}
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)
return decoded.results.first?.version
🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Repository/AppConfigRepository.swift around lines
18 to 21, the code does not check the HTTP response status code after the
network request, which can cause issues if the server returns an error or
invalid response. Modify the code to capture the URLResponse from the data task,
cast it to HTTPURLResponse, and verify that the status code indicates success
(e.g., 200-299). If the status code is outside this range, throw an appropriate
error or handle the failure before attempting to decode the data.

}
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
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ enum BitnagilGraphic {
static let onboardingGraphic = UIImage(named: "onboarding_graphic", in: bundle, with: nil)
static let defaultEmotionGraphic = UIImage(named: "default_emotion_graphic", in: bundle, with: nil)
static let logoGraphic = UIImage(named: "bitnagil_logo", in: bundle, with: nil)
static let profileGraphic = UIImage(named: "profile_graphic", in: bundle, with: nil)
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ extension UIViewController {

case .withPrograssBarWithoutBackButton(let step, let stepCount):
navigationController?.setNavigationBarHidden(false, animated: false)
navigationController?.navigationItem.setHidesBackButton(true, animated: false)
navigationItem.setHidesBackButton(true, animated: false)
configureProgressNavigationBar(step: step, stepCount: stepCount)
}
}
Expand Down
4 changes: 1 addition & 3 deletions Projects/Presentation/Sources/MyPage/View/MypageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ final class MypageView: BaseViewController<MypageViewModel> {
.settingIcon?
.withRenderingMode(.alwaysTemplate)

profileImageView.layer.cornerRadius = Layout.profileImageViewCornerRadius
profileImageView.layer.masksToBounds = true
profileImageView.backgroundColor = BitnagilColor.gray40 // 임시
profileImageView.image = BitnagilGraphic.profileGraphic

nicknameLabel.font = BitnagilFont(style: .title3, weight: .semiBold).font
nicknameLabel.textColor = .black
Expand Down
135 changes: 81 additions & 54 deletions Projects/Presentation/Sources/Setting/View/SettingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
//

import Combine
import Domain
import SafariServices
import Shared
import SnapKit
import UIKit

Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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() {
Expand All @@ -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)

Expand All @@ -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

@coderabbitai coderabbitai Bot Aug 7, 2025

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

사용하지 않는 클로저 파라미터를 _로 대체

isAuthenticated 파라미터를 사용하지 않으므로 _로 대체해야 합니다.

-            .sink(receiveValue: { isAuthenticated in
+            .sink(receiveValue: { _ in
📝 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
.sink(receiveValue: { isAuthenticated in
.sink(receiveValue: { _ in
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 204-204: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)

🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Setting/View/SettingView.swift at line 204, the
closure parameter named isAuthenticated is not used inside the closure body.
Replace the parameter name isAuthenticated with an underscore (_) to indicate
that the parameter is intentionally unused.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

아하 ! 아니면 logout, withdraw 시에 false를 보내니까 false 받았을 때에만 IntroView로 이동하는 것은 어떠신가요 ??

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@choijungp 좋은 제안입니다! 맞습니다. 로그아웃이나 탈퇴 시에만 IntroView로 이동해야 하므로, isAuthenticatedfalse일 때에만 화면 전환을 하는 것이 더 정확한 로직이네요.

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
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift:0-0
Timestamp: 2025-07-16T10:57:16.559Z
Learning: choijungp는 추후 CustomAlertView를 만들어서 에러 처리를 UI로 해결하도록 수정할 계획을 가지고 있음.

Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift:0-0
Timestamp: 2025-07-16T10:58:30.858Z
Learning: choijungp는 OnboardingUseCase의 convertToDictionary 함수에서 에러 처리 개선을 추후 고민하고 반영할 계획을 가지고 있음.

Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#30
File: Projects/DataSource/Sources/NetworkService/Plugin/RefreshTokenPlugin.swift:42-43
Timestamp: 2025-07-30T03:56:18.617Z
Learning: choijungp는 현재 테스트 단계에서 RefreshTokenPlugin의 토큰 갱신 로직 디버깅을 위해 액세스 토큰과 리프레시 토큰의 실제 값을 로그에 기록하는 것을 선호함.

// 로그아웃 완료 후 홈 화면으로
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)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
Loading
Loading