Skip to content
Merged
4 changes: 4 additions & 0 deletions MLS/.swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
excluded:
- "**/.build"
- "**/SourcePackages"

# 기본(default) 룰이 아닌 룰들을 활성화
opt_in_rules:
- sorted_imports
Expand Down
328 changes: 174 additions & 154 deletions MLS/MLS.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

24 changes: 19 additions & 5 deletions MLS/MLS/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import BaseFeature
import BookmarkFeatureInterface
import DesignSystem
import DictionaryFeatureInterface
import MLSRecommendationFeatureInterface
import MyPageFeatureInterface

import RxSwift

public final class AppCoordinator: AppCoordinatorProtocol {
// MARK: - Properties
public var window: UIWindow?
private let recommendationMainFactory: RecommendationMainFactory
private let dictionaryMainViewFactory: DictionaryMainViewFactory
private let bookmarkMainFactory: BookmarkMainFactory
private let myPageMainFactory: MyPageMainFactory
Expand All @@ -23,12 +25,14 @@ public final class AppCoordinator: AppCoordinatorProtocol {
// MARK: - Init
public init(
window: UIWindow?,
recommendationMainFactory: RecommendationMainFactory,
dictionaryMainViewFactory: DictionaryMainViewFactory,
bookmarkMainFactory: BookmarkMainFactory,
myPageMainFactory: MyPageMainFactory,
loginFactory: LoginFactory
) {
self.window = window
self.recommendationMainFactory = recommendationMainFactory
self.dictionaryMainViewFactory = dictionaryMainViewFactory
self.bookmarkMainFactory = bookmarkMainFactory
self.myPageMainFactory = myPageMainFactory
Expand All @@ -37,11 +41,21 @@ public final class AppCoordinator: AppCoordinatorProtocol {

// MARK: - Public Methods
public func showMainTab() {
let tabBar = BottomTabBarController(viewControllers: [
dictionaryMainViewFactory.make(),
bookmarkMainFactory.make(),
myPageMainFactory.make()
])
let tabItems: [TabItem] = [
TabItem(title: "추천", icon: UIImage(systemName: "star.fill") ?? UIImage()),
TabItem(title: "도감", icon: DesignSystemAsset.image(named: "dictionary") ?? UIImage()),
TabItem(title: "북마크", icon: DesignSystemAsset.image(named: "bookmarkList") ?? UIImage()),
TabItem(title: "MY", icon: DesignSystemAsset.image(named: "mypage") ?? UIImage())
]
let tabBar = BottomTabBarController(
viewControllers: [
recommendationMainFactory.make(),
dictionaryMainViewFactory.make(),
bookmarkMainFactory.make(),
myPageMainFactory.make()
],
tabItems: tabItems
)

let navigationController = UINavigationController(rootViewController: tabBar)
navigationController.isNavigationBarHidden = true
Expand Down
13 changes: 13 additions & 0 deletions MLS/MLS/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import Domain
import DomainInterface
import Firebase
import KakaoSDKCommon
import MLSRecommendationFeature
import MLSRecommendationFeatureInterface
import MyPageFeature
import MyPageFeatureInterface
import os
Expand Down Expand Up @@ -127,6 +129,9 @@ private extension AppDelegate {
DIContainer.register(type: AppCoordinatorProtocol.self) {
AppCoordinator(
window: nil,
recommendationMainFactory: DIContainer.resolve(
type: RecommendationMainFactory.self
),
dictionaryMainViewFactory: DIContainer.resolve(
type: DictionaryMainViewFactory.self
),
Expand Down Expand Up @@ -214,6 +219,9 @@ private extension AppDelegate {
tokenInterceptor: DIContainer.resolve(type: Interceptor.self, name: "tokenInterceptor")
)
}
DIContainer.register(type: RecommendationRepository.self) {
RecommendationRepositoryImpl()
}
}

func registerUseCase() {
Expand Down Expand Up @@ -1248,5 +1256,10 @@ private extension AppDelegate {
DIContainer.register(type: PolicyFactory.self) {
PolicyFactoryImpl()
}
DIContainer.register(type: RecommendationMainFactory.self) {
RecommendationMainFactoryImpl(
repository: DIContainer.resolve(type: RecommendationRepository.self)
)
}
}
}
35 changes: 10 additions & 25 deletions MLS/MLSCore/Sources/MLSCore/Network/NetworkProviderImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

import RxSwift

public final class NetworkProviderImpl: NetworkProvider {
public final class NetworkProviderImpl: NetworkProvider, Loggable {

private let session: URLSession

Expand All @@ -17,32 +17,32 @@ public final class NetworkProviderImpl: NetworkProvider {

public func requestData<T: Responsable & Requestable>(endPoint: T, interceptor: Interceptor?) -> Observable<T.Response> {
return Observable.create { [weak self] observer in
print("🚀 requestData: 요청 시작 - \(endPoint)")
self?.logDebug("Core requestData: 요청 시작 - \(endPoint)")

self?.sendRequest(endPoint: endPoint, interceptor: interceptor, completion: { result in

switch result {
case .success(let data):
print("✅ requestData: 응답 수신")
self?.logDebug("Core requestData: 응답 수신")

if let data = data {
print("📦 requestData: 응답 데이터 있음 - \(String(data: data, encoding: .utf8) ?? "디코딩 실패")")
self?.logDebug("Core requestData: 응답 데이터 있음 - \(String(data: data, encoding: .utf8) ?? "디코딩 실패")")
do {
let decoded = try JSONDecoder().decode(T.Response.self, from: data)
print("🎯 requestData: 디코딩 성공 - \(decoded)")
self?.logDebug("Core requestData: 디코딩 성공 - \(decoded)")
observer.onNext(decoded)
observer.onCompleted()
} catch {
print("❌ requestData: 디코딩 실패 - \(error)")
self?.logError("Core requestData: 디코딩 실패 - \(error)")
observer.onError(NetworkError.decodeError(error))
}
} else {
print("⚠️ requestData: 응답 데이터 없음")
self?.logWarning("Core requestData: 응답 데이터 없음")
observer.onError(NetworkError.noData)
}

case .failure(let error):
print("🔥 requestData: 네트워크 실패 - \(error)")
self?.logError("🔥 requestData: 네트워크 실패 - \(error)")
observer.onError(error)
}
})
Expand All @@ -53,7 +53,7 @@ public final class NetworkProviderImpl: NetworkProvider {
errors
.enumerated()
.flatMap { attempt, error -> Observable<Void> in
print("🔁 requestData: 재시도 \(attempt + 1)회 - 에러: \(error)")
self.logWarning("🔁 requestData: 재시도 \(attempt + 1)회 - 에러: \(error)")
if attempt < self.retryAttempt, let networkError = error as? NetworkError, networkError == .retry {
return Observable.just(())
} else {
Expand Down Expand Up @@ -94,10 +94,6 @@ public final class NetworkProviderImpl: NetworkProvider {
}

private extension NetworkProviderImpl {
/// 엔드 포인트를 이용하여 요청을 보내기 위한 함수
/// - Parameters:
/// - endPoint: 요청을 위한 엔드포인트 객체
/// - completion: 응답 결과
func sendRequest<T: Requestable>(endPoint: T, interceptor: Interceptor?, completion: @escaping (Result<Data?, NetworkError>) -> Void) {
do {
var request = try endPoint.getUrlRequest()
Expand All @@ -113,7 +109,7 @@ private extension NetworkProviderImpl {
completion(.success(data))
case .failure(let error):
completion(.failure(error))
print("API 통신에러 \(error)")
logError("API 통신에러 \(error)")
}
}
task.resume()
Expand All @@ -122,35 +118,25 @@ private extension NetworkProviderImpl {
}
}

/// 통신간의 유효성 검사를 위한 함수
/// - Parameters:
/// - data: 통신 결과로 돌려받은 데이터
/// - response: 상태코드를 포함한 통신 응답
/// - error: 통신간에 발생한 에러
/// - Returns: 유효성검사 결과에 따른 데이터와 에러
func checkValidation(
data: Data?,
response: URLResponse?,
error: Error?,
interceptor: Interceptor?
) -> Result<Data?, NetworkError> {

// 1️⃣ 네트워크 레벨 에러 먼저 체크
if let error {
if let urlError = error as? URLError, urlError.code == .unsupportedURL {
return .failure(.urlRequest(error))
}
return .failure(.network(error))
}

// 2️⃣ HTTP 응답 객체 확인
guard let httpResponse = response as? HTTPURLResponse else {
return .failure(.httpError)
}

// 3️⃣ 상태 코드 기반 검사
guard (200 ... 299).contains(httpResponse.statusCode) else {
// ❗️여기서만 인터셉터 개입
if let interceptor = interceptor,
interceptor.retry(data: data, response: response, error: error) {
return .failure(.retry)
Expand All @@ -160,7 +146,6 @@ private extension NetworkProviderImpl {
return .failure(.statusError(httpResponse.statusCode, errorMessage))
}

// ✅ 성공 응답
return .success(data)
}
}
32 changes: 32 additions & 0 deletions MLS/MLSCore/Sources/MLSCore/Network/TokenInterceptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation
import Security

public final class TokenInterceptor: Interceptor {

public init() {}

public func adapt(_ request: URLRequest) -> URLRequest {
guard let token = fetchAccessToken() else { return request }
var adapted = request
adapted.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
return adapted
}

public func retry(data: Data?, response: URLResponse?, error: Error?) -> Bool {
return false
}

private func fetchAccessToken() -> String? {
let query: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: "keyChain",
kSecAttrAccount: "accessToken",
Comment thread
dongglehada marked this conversation as resolved.
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne
]
var ref: AnyObject?
guard SecItemCopyMatching(query, &ref) == errSecSuccess,
let data = ref as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public final class CardList: UIView {
static let iconSize: CGFloat = 24
static let mapImageSize: CGFloat = 40
static let tagHeight: CGFloat = 24
static let tagHorizontalInset: CGFloat = 10
static let tagVerticalInset: CGFloat = 4
}

// MARK: - Properties
Expand Down Expand Up @@ -78,7 +80,7 @@ public final class CardList: UIView {
public let imageView = ItemImageView(image: nil, cornerRadius: Constant.imageRadius, inset: Constant.imageInset, backgroundColor: .listMap)

private lazy var textLabelStackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [rankTag, mainTextLabel, subTextLabel])
let view = UIStackView(arrangedSubviews: [rankContainer, mainTextLabel, subTextLabel])
view.axis = .vertical
view.spacing = Constant.stackViewSpacing
view.alignment = .leading
Expand Down Expand Up @@ -129,7 +131,20 @@ public final class CardList: UIView {

private let badge = Badge(style: .currentQuest)

private let rankTag = TagChip(style: .text, text: "순위")
private let rankContainer = {
let view = UIView()
view.backgroundColor = .primary50
view.layer.cornerRadius = 12
view.clipsToBounds = true
return view
}()

private let rankTag = {
let label = UILabel()
label.font = .korFont(style: .semiBold, size: 14)
label.textColor = .primary700
return label
}()

public init() {
super.init(frame: .zero)
Expand All @@ -153,6 +168,7 @@ private extension CardList {
addSubview(iconButton)
addSubview(dropInfoStack)
addSubview(badge)
rankContainer.addSubview(rankTag)
}

func setupConstraints() {
Expand All @@ -177,10 +193,15 @@ private extension CardList {
make.trailing.equalToSuperview().inset(Constant.cardTrailingInset)
}

rankTag.snp.makeConstraints { make in
rankContainer.snp.makeConstraints { make in
make.height.equalTo(Constant.tagHeight)
}

rankTag.snp.makeConstraints { make in
make.horizontalEdges.equalToSuperview().inset(Constant.tagHorizontalInset)
make.verticalEdges.equalToSuperview().inset(Constant.tagVerticalInset)
}

iconButton.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalToSuperview().inset(Constant.cardTrailingInset)
Expand Down Expand Up @@ -250,24 +271,24 @@ public extension CardList {
iconButton.isHidden = true
dropInfoStack.isHidden = true
badge.isHidden = true
rankTag.isHidden = true
rankContainer.isHidden = true
case .detailStackText:
iconButton.isHidden = true
dropInfoStack.isHidden = false
badge.isHidden = true
rankTag.isHidden = true
rankContainer.isHidden = true
case .detailStackBadge(let type):
iconButton.isHidden = true
dropInfoStack.isHidden = false
badge.isHidden = false
rankTag.isHidden = true
rankContainer.isHidden = true
badge.update(style: type)
case .recommended(let rank):
iconButton.isHidden = false
dropInfoStack.isHidden = true
subTextLabel.isHidden = true
badge.isHidden = true
rankTag.isHidden = false
rankContainer.isHidden = false
rankTag.text = "\(rank)위"
default:
iconButton.isHidden = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ public final class BottomTabBarController: UITabBarController {
private let customTabBar: BottomTabBar

// MARK: - Init
public init(viewControllers: [UIViewController], initialIndex: Int = 0) {
tabItems = [
public init(viewControllers: [UIViewController], tabItems: [TabItem]? = nil, initialIndex: Int = 0) {
let resolvedItems = tabItems ?? [
TabItem(title: "도감", icon: DesignSystemAsset.image(named: "dictionary")),
TabItem(title: "북마크", icon: DesignSystemAsset.image(named: "bookmarkList")),
TabItem(title: "MY", icon: DesignSystemAsset.image(named: "mypage"))
]
customTabBar = BottomTabBar(tabItems: tabItems, selectedIndex: initialIndex)
self.tabItems = resolvedItems
customTabBar = BottomTabBar(tabItems: resolvedItems, selectedIndex: initialIndex)
super.init(nibName: nil, bundle: nil)
configureUI(controllers: viewControllers)
selectedIndex = initialIndex
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ final class TooltipView: UIView {
init(text: String, tooltipPosition: TooltipPosition) {
self.tooltipPosition = tooltipPosition
super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
addViews()
setupConstraints()
configureUI(text: text)
Expand Down
Loading