Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
39362ed
fix/#339: AppCoordinator 타입 불일치 수정 (AppCoordinator -> AppCoordinatorP…
dongglehada Jun 9, 2026
66e674c
fix/#339: 추천 기능 DTO 이중 래핑 제거 및 에러 로깅 추가
dongglehada Jun 9, 2026
c213889
feat/#339: 추천 화면 비로그인 상태 뷰 및 수정하기 네비게이션 추가
dongglehada Jun 9, 2026
3a0f337
feat/#339: 추천 화면 헤더 검색/알림 버튼 네비게이션 추가
dongglehada Jun 9, 2026
5bd524c
refactor/#339: LoginExitRoute를 MLSAppFeatureInterface에서 MLSCore로 이동
dongglehada Jun 10, 2026
620125f
feat/#339: 추천 화면 북마크 기능 구현
dongglehada Jun 10, 2026
2404af2
feat/#339: 탭바 자동 숨김 처리 (push 시 숨김, root 복귀 시 표시)
dongglehada Jun 10, 2026
56748f4
fix/#339: MLSDesignSystem SPM 모듈 에셋 번들 참조 오류 수정 및 탭바 배경 추가
dongglehada Jun 10, 2026
c88f23b
feat/#339: 앱 버전 업데이트 체크 및 얼럿 구현
dongglehada Jun 10, 2026
27defeb
style/#339: Apply SwiftLint autocorrect
github-actions[bot] Jun 10, 2026
93ef498
fix/#339: 도감 상세 도감 버튼 탭 이동 인덱스 수정 (추천→도감)
dongglehada Jun 10, 2026
18c0c4d
fix/#339: 캐릭터 수정 화면 기존 레벨/직업 데이터 미입력 수정
dongglehada Jun 10, 2026
e8e7e1f
fix/#339: 프로필 이미지 선택 모달 열릴 때 탭바 재노출 수정
dongglehada Jun 10, 2026
ea8aeb9
fix/#339: 프로필 닉네임 수정 취소 시 미저장 값이 표시되는 문제 수정
dongglehada Jun 10, 2026
f03ca2f
fix/#339: 강제 업데이트 옵저버 중복 등록으로 인한 메모리 누수 수정
dongglehada Jun 10, 2026
2971e33
fix/#339: 북마크 버튼 연속 탭 시 중복 API 요청 방지
dongglehada Jun 10, 2026
0ab1485
fix/#339: 프로필 이미지 선택 모달 닫힐 때 탭바 재노출 수정
dongglehada Jun 10, 2026
2dcb87b
fix/#339: 프로필 이미지 즉시 저장 제거, 완료 버튼에서 닉네임+이미지 함께 저장 및 편집 모드 종료
dongglehada Jun 10, 2026
32b3b93
fix/#339: 프로필 편집 모드 진입 시 완료 버튼 동작 불가 수정 및 뒤로가기 버튼 숨김 처리
dongglehada Jun 10, 2026
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: 7 additions & 0 deletions MLS/MLS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
085A7F752DAF99570046663F /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 085A7F742DAF99570046663F /* .swiftlint.yml */; };
088636972FB4664E00006D4A /* MLSRecommendationFeatureTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 088636962FB4664E00006D4A /* MLSRecommendationFeatureTesting */; };
0898D45E2FD913470031ED17 /* MLSBookmarkFeatureTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 0898D45D2FD913470031ED17 /* MLSBookmarkFeatureTesting */; };
08DA51B42E1B9827009097A6 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 08DA51B32E1B9827009097A6 /* FirebaseFirestore */; };
08DA51B62E1B9827009097A6 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 08DA51B52E1B9827009097A6 /* FirebaseMessaging */; };
08ED49282DCFDED4002C21A2 /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 08ED49272DCFDED4002C21A2 /* RxCocoa */; };
Expand Down Expand Up @@ -289,6 +290,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0898D45E2FD913470031ED17 /* MLSBookmarkFeatureTesting in Frameworks */,
08F7DC802F9DEA8100EF5C06 /* MLSRecommendationFeature in Frameworks */,
08F7DC822F9DEA8100EF5C06 /* MLSRecommendationFeatureInterface in Frameworks */,
088636972FB4664E00006D4A /* MLSRecommendationFeatureTesting in Frameworks */,
Expand Down Expand Up @@ -561,6 +563,7 @@
08F7DC832F9DEA8100EF5C06 /* MLSRecommendationFeatureInterface */,
08F7DC852F9DEA8100EF5C06 /* MLSCore */,
088636962FB4664E00006D4A /* MLSRecommendationFeatureTesting */,
0898D45D2FD913470031ED17 /* MLSBookmarkFeatureTesting */,
);
productName = MLSRecommendationFeatureExample;
productReference = 08F7DC612F9DEA8000EF5C06 /* MLSRecommendationFeatureExample.app */;
Expand Down Expand Up @@ -1785,6 +1788,10 @@
isa = XCSwiftPackageProductDependency;
productName = MLSRecommendationFeatureTesting;
};
0898D45D2FD913470031ED17 /* MLSBookmarkFeatureTesting */ = {
isa = XCSwiftPackageProductDependency;
productName = MLSBookmarkFeatureTesting;
};
08DA51B32E1B9827009097A6 /* FirebaseFirestore */ = {
isa = XCSwiftPackageProductDependency;
package = 08DA51B22E1B9827009097A6 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
Expand Down
13 changes: 0 additions & 13 deletions MLS/MLS/Application/ViewController.swift

This file was deleted.

4 changes: 3 additions & 1 deletion MLS/MLSAppFeature/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ let package = Package(
// Interface 모듈 (도메인 모델 및 프로토콜)
.target(
name: "MLSAppFeatureInterface",
dependencies: []
dependencies: [
.product(name: "MLSCore", package: "MLSCore")
]
),
// Feature 모듈 (실제 구현)
.target(
Expand Down
5 changes: 5 additions & 0 deletions MLS/MLSAppFeature/Sources/MLSAppFeature/AppInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
enum AppInfo {
// TODO: 앱스토어 출시 후 실제 앱 ID로 교체
static let appStoreID = "6477212894"
static let appStoreURL = "itms-apps://itunes.apple.com/app/id\(appStoreID)"
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public final class AppCoordinator: AppCoordinatorProtocol {
}

// MARK: - Public Methods
public func showMainTab() {
public func showMainTab(selectedIndex: Int = 0) {
let tabItems: [TabItem] = [
TabItem(title: "추천", icon: UIImage(systemName: "star.fill") ?? UIImage()),
TabItem(title: "도감", icon: DesignSystemAsset.image(named: "dictionary")),
Expand All @@ -56,7 +56,8 @@ public final class AppCoordinator: AppCoordinatorProtocol {
bookmarkMainFactory.make(bottomInset: 64),
myPageMainFactory.make()
],
tabItems: tabItems
tabItems: tabItems,
initialIndex: selectedIndex
)

let navigationController = UINavigationController(rootViewController: tabBar)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public enum FactoryAssembly {
type: DetailOnBoardingFactory.self
),
appCoordinator: {
DIContainer.resolve(type: AppCoordinator.self)
DIContainer.resolve(type: AppCoordinatorProtocol.self)
},
dictionaryDetailAPIRepository: DIContainer.resolve(
type: DictionaryDetailAPIRepository.self
Expand Down Expand Up @@ -415,15 +415,14 @@ public enum FactoryAssembly {
DIContainer
.resolve(type: CheckValidLevelUseCase.self),
authRepository: DIContainer
.resolve(type: AuthAPIRepository.self)
)
}
DIContainer.register(type: SelectImageFactory.self) {
SelectImageFactoryImpl(
.resolve(type: AuthAPIRepository.self),
myPageRepository: DIContainer
.resolve(type: MyPageRepository.self)
)
}
DIContainer.register(type: SelectImageFactory.self) {
SelectImageFactoryImpl()
}
DIContainer.register(type: DetailOnBoardingFactory.self) {
DetailOnBoardingFactoryImpl()
}
Expand All @@ -432,9 +431,30 @@ public enum FactoryAssembly {
}
DIContainer.register(type: RecommendationMainFactory.self) {
RecommendationMainFactoryImpl(
repository: DIContainer.resolve(
type: RecommendationRepository.self
)
repository: DIContainer.resolve(type: RecommendationRepository.self),
bookmarkRepository: DIContainer.resolve(type: BookmarkRepository.self),
makeLoginVC: {
DIContainer.resolve(type: LoginFactory.self).make(exitRoute: .pop)
},
makeCharacterSettingVC: {
DIContainer.resolve(type: SetCharacterFactory.self).make()
},
makeSearchVC: {
DIContainer.resolve(type: DictionarySearchFactory.self).make()
},
makeNotificationVC: {
DIContainer.resolve(type: DictionaryNotificationFactory.self).make()
},
makeDetailVC: { mapId in
DIContainer.resolve(type: DictionaryDetailFactory.self).make(
type: .map, id: mapId, bookmarkRelay: nil, loginRelay: nil
)
},
makeBookmarkModalVC: { bookmarkIds, onComplete in
DIContainer.resolve(type: BookmarkModalFactory.self).make(
bookmarkIds: bookmarkIds, onComplete: onComplete
)
}
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import MLSAppFeatureInterface
import MLSAuthFeature
import MLSAuthFeatureInterface
import MLSBookmarkFeature
Expand Down Expand Up @@ -80,5 +81,11 @@ public enum RepositoryAssembly {
DIContainer.register(type: BookmarkUserDefaultsRepository.self) {
BookmarkUserDefaultsRepositoryImpl()
}
DIContainer.register(type: AppStoreRepositoryProtocol.self) {
AppStoreRepository()
}
DIContainer.register(type: UpdateSkipRepositoryProtocol.self) {
UpdateSkipRepository()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import MLSAppFeatureInterface
import MLSAuthFeature
import MLSAuthFeatureInterface
import MLSCore
Expand Down Expand Up @@ -82,5 +83,12 @@ public enum UseCaseAssembly {
.resolve(type: UserDefaultsRepository.self, name: "authUserDefaultsRepository")
)
}
DIContainer.register(type: UpdateCheckerUseCaseProtocol.self) {
UpdateCheckerUseCase(
appID: AppInfo.appStoreID,
appStoreRepository: DIContainer.resolve(type: AppStoreRepositoryProtocol.self),
skipRepository: DIContainer.resolve(type: UpdateSkipRepositoryProtocol.self)
)
}
}
}
72 changes: 70 additions & 2 deletions MLS/MLSAppFeature/Sources/MLSAppFeature/Launcher/AppLauncher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import UIKit
import MLSAppFeatureInterface
import MLSAuthFeatureInterface
import MLSCore
import MLSDesignSystem

import RxSwift

@MainActor
public final class AppLauncher {
private let disposeBag = DisposeBag()
private var forceUpdateObserver: NSObjectProtocol?

public init() {}

Expand All @@ -26,16 +28,18 @@ public final class AppLauncher {
authRepository.reissueToken(refreshToken: refreshToken)
.observe(on: MainScheduler.instance)
.subscribe(
onNext: { _ in
onNext: { [weak self] _ in
Task { @MainActor in
let coordinator = DIContainer.resolve(type: AppCoordinatorProtocol.self)
coordinator.showMainTab()
self?.checkUpdateIfNeeded()
}
},
onError: { _ in
onError: { [weak self] _ in
Task { @MainActor in
let coordinator = DIContainer.resolve(type: AppCoordinatorProtocol.self)
coordinator.showLogin(exitRoute: .home)
self?.checkUpdateIfNeeded()
}
}
)
Expand All @@ -44,10 +48,74 @@ public final class AppLauncher {
case .failure:
let coordinator = DIContainer.resolve(type: AppCoordinatorProtocol.self)
coordinator.showLogin(exitRoute: .home)
checkUpdateIfNeeded()
}
}

public func register() {
DependencyAssembler.register()
}
}

// MARK: - Update Check
private extension AppLauncher {
func checkUpdateIfNeeded() {
guard
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
let currentVersion = Version(versionString: versionString)
else { return }

Task { @MainActor in
let useCase = DIContainer.resolve(type: UpdateCheckerUseCaseProtocol.self)
guard let status = try? await useCase.checkUpdate(currentVersion: currentVersion) else { return }

switch status {
case .force:
showForceUpdateAlert()
if let existingObserver = forceUpdateObserver {
NotificationCenter.default.removeObserver(existingObserver)
}
forceUpdateObserver = NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.showForceUpdateAlert()
}
Comment on lines +73 to +84

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

NotificationCenter의 블록 기반 addObserver는 반환된 옵저버 토큰을 명시적으로 제거하지 않으면 메모리 누수가 발생할 수 있습니다. AppLauncher가 해제되거나 새로운 옵저버를 등록하기 전에 기존 옵저버를 NotificationCenter.default.removeObserver를 통해 제거해주는 것이 안전합니다.

            case .force:
                showForceUpdateAlert()
                if let existingObserver = forceUpdateObserver {
                    NotificationCenter.default.removeObserver(existingObserver)
                }
                forceUpdateObserver = NotificationCenter.default.addObserver(
                    forName: UIApplication.didBecomeActiveNotification,
                    object: nil,
                    queue: .main
                ) { [weak self] _ in
                    self?.showForceUpdateAlert()
                }

case .optional(let latestVersion):
GuideAlertFactory.show(
mainText: "앱 버전이 달라요.\n보다 좋은 서비스를 위해 업데이트 해주세요.",
ctaText: "업데이트 하기",
cancelText: "나중에",
ctaAction: { [weak self] in self?.openAppStore() },
cancelAction: { useCase.skipUpdate(version: latestVersion) }
)
case .none:
break
}
}
}

func showForceUpdateAlert() {
GuideAlertFactory.show(
mainText: "앱 버전이 달라요.\n보다 좋은 서비스를 위해 업데이트 해주세요.",
ctaText: "업데이트 하기",
cancelText: nil,
ctaAction: { [weak self] in self?.openAppStore() }
)
}

func openAppStore() {
guard let url = URL(string: AppInfo.appStoreURL) else {
showForceUpdateAlert()
return
}
UIApplication.shared.open(url, options: [:]) { [weak self] success in
if !success {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self?.showForceUpdateAlert()
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import MLSCore
import UIKit

@MainActor
public protocol AppCoordinatorProtocol: AnyObject {
var window: UIWindow? { get set }
func showMainTab()
func showMainTab(selectedIndex: Int)
func showLogin(exitRoute: LoginExitRoute)
}

public extension AppCoordinatorProtocol {
func showMainTab() {
showMainTab(selectedIndex: 0)
}
}
1 change: 0 additions & 1 deletion MLS/MLSAuthFeature/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ let package = Package(
.target(
name: "MLSAuthFeatureInterface",
dependencies: [
.product(name: "MLSAppFeatureInterface", package: "MLSAppFeature"),
.product(name: "MLSCore", package: "MLSCore"),
.product(name: "MLSDesignSystem", package: "MLSDesignSystem"),
.product(name: "RxSwift", package: "RxSwift")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import MLSAppFeatureInterface
import MLSCore

public protocol LoginFactory {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import UIKit

public enum LoginExitRoute {
case pop
case home
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ private extension DictionaryDetailListView {

func makeButton(label: UILabel) -> UIButton {
let button = UIButton()
let icon = UIImageView(image: UIImage(named: "rightArrow"))
let icon = UIImageView(image: DesignSystemAsset.image(named: "rightArrow"))

button.addSubview(label)
button.addSubview(icon)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,17 @@ extension DropDownBox: UITableViewDataSource, UITableViewDelegate {
}
}

// MARK: - Public Methods
public extension DropDownBox {
func selectItem(id: Int) {
guard let index = items.firstIndex(where: { $0.id == id }) else { return }
selectedIndex = index
inputBox.textField.attributedText = .makeStyledString(
font: .b_m_r, text: items[index].name, alignment: .left, lineHeight: 1.0
)
}
}

// MARK: Model
extension DropDownBox {
public struct Item {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public final class ErrorMessage: UIView {
// MARK: - Properties
private let iconView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "error")
view.image = DesignSystemAsset.image(named: "error")
return view
}()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public final class FloatingActionButton: UIButton {
// MARK: - SetUp
private extension FloatingActionButton {
func configureUI() {
setImage(UIImage(named: "fab"), for: .normal)
setImage(DesignSystemAsset.image(named: "fab"), for: .normal)
layer.cornerRadius = 24
clipsToBounds = true
addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class GuideAlert: UIView {
// MARK: - Components
private let warningIconView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "warning")
view.image = DesignSystemAsset.image(named: "warning")
return view
}()

Expand All @@ -34,7 +34,7 @@ public class GuideAlert: UIView {

// MARK: - init
public init(mainText: String, ctaText: String, cancelText: String?, ctaRatio: Double = 0.7) {
mainTextLabel.attributedText = .makeStyledString(font: .sub_l_b, text: mainText)
mainTextLabel.attributedText = .makeStyledString(font: .sub_m_sb, text: mainText)
self.ctaButton = CommonButton(style: .normal, title: ctaText, disabledTitle: nil)
self.cancelButton = cancelText.map { CommonButton(style: .border, title: $0, disabledTitle: nil) }
mainTextLabel.numberOfLines = 0
Expand Down
Loading
Loading