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
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
Loading