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: 2 additions & 2 deletions FLINT/Data/Sources/DTO/Base/BaseResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public struct BaseResponse<T: Codable>: Codable {
public let additionalInfo: [String: String]?

public let status: Int
public let message: String
public let message: String?
public let data: T?
}

Expand All @@ -36,7 +36,7 @@ extension BaseResponse {
errorCode: errorCode ?? "",
additionalInfo: additionalInfo ?? [:],
status: status,
message: message
message: message ?? ""
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// RecalculateKeywordsUseCase.swift
// Domain
//
// Created by 진소은 on 6/20/26.
//

import Combine
import Foundation

import Repository

public protocol RecalculateKeywordsUseCase {
func callAsFunction() -> AnyPublisher<Void, Error>
}

public final class DefaultRecalculateKeywordsUseCase: RecalculateKeywordsUseCase {

private let userRepository: UserRepository

public init(userRepository: UserRepository) {
self.userRepository = userRepository
}

public func callAsFunction() -> AnyPublisher<Void, Error> {
return userRepository.recalculateMyKeywords()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// RecalculateKeywordsUseCaseFactory.swift
// FLINT
//
// Created by 진소은 on 6/20/26.
//

import Foundation

import Domain

protocol RecalculateKeywordsUseCaseFactory: UserRepositoryFactory {
func makeRecalculateKeywordsUseCase() -> RecalculateKeywordsUseCase
}

extension RecalculateKeywordsUseCaseFactory {
func makeRecalculateKeywordsUseCase() -> RecalculateKeywordsUseCase {
return DefaultRecalculateKeywordsUseCase(userRepository: makeUserRepository())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@ import Foundation
import Domain
import Presentation

protocol ProfileViewModelFactory: FetchProfileUseCaseFactory, FetchKeywordsUseCaseFactory, FetchCreatedCollectionsUseCaseFactory, FetchBookmarkedCollectionsUseCaseFactory, FetchBookmarkedContentsUseCaseFactory {
protocol ProfileViewModelFactory: FetchProfileUseCaseFactory, FetchKeywordsUseCaseFactory, FetchCreatedCollectionsUseCaseFactory, FetchBookmarkedCollectionsUseCaseFactory, FetchBookmarkedContentsUseCaseFactory, RecalculateKeywordsUseCaseFactory {
func makeProfileViewModel(target: UserTarget) -> ProfileViewModel
}

extension ProfileViewModelFactory {
func makeProfileViewModel(target: UserTarget) -> ProfileViewModel {
return ProfileViewModel(target: target, fetchProfileUseCase: makeFetchProfileUseCase(), fetchKeywordsUseCase: makeFetchKeywordsUseCase(), fetchCreatedCollectionsUseCase: makeFetchCreatedCollectionsUseCase(), fetchBookmarkedCollectionsUseCase: makeFetchBookmarkedCollectionsUseCase(), fetchBookmarkedContentsUseCase: makeFetchBookmarkedContentsUseCase())
return ProfileViewModel(
target: target,
fetchProfileUseCase: makeFetchProfileUseCase(),
fetchKeywordsUseCase: makeFetchKeywordsUseCase(),
fetchCreatedCollectionsUseCase: makeFetchCreatedCollectionsUseCase(),
fetchBookmarkedCollectionsUseCase: makeFetchBookmarkedCollectionsUseCase(),
fetchBookmarkedContentsUseCase: makeFetchBookmarkedContentsUseCase(),
recalculateKeywordsUseCase: makeRecalculateKeywordsUseCase()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,98 +11,251 @@ import SnapKit
import Then

public final class TitleHeaderTableViewCell: BaseTableViewCell {

// MARK: - Type

public enum TitleHeaderStyle {
case normal
case more
}

// MARK: - Public Event

public var onTapMore: (() -> Void)?

public var onTapInfo: (() -> Void)?
public var onTapRefresh: (() -> Void)?

// MARK: - UI

private let middleGuideView = UIView()


private let titleStack = UIStackView().then {
$0.axis = .horizontal
$0.alignment = .center
$0.spacing = 4
}

private let titleLabel = UILabel().then {
$0.textColor = .flintWhite
$0.numberOfLines = 1
}


private let infoButton = UIButton().then {
$0.setImage(UIImage(resource: .icInfo), for: .normal)
$0.tintColor = .flintWhite
$0.isHidden = true
}

private let subtitleLabel = UILabel().then {
$0.textColor = .flintGray200
$0.numberOfLines = 1
}


private let middleGuideView = UIView()

let moreButton = UIButton().then {
$0.setImage(.icMore, for: .normal)
$0.isHidden = true
}


private let refreshStack = UIStackView().then {
$0.axis = .vertical
$0.alignment = .center
$0.spacing = 4
$0.isHidden = true
$0.isUserInteractionEnabled = true
}

private let refreshIconImageView = UIImageView().then {
$0.image = UIImage(named: "ic_refresh", in: .module, with: nil)
$0.tintColor = DesignSystem.Color.secondary400
$0.contentMode = .scaleAspectFit
}

private let refreshLabel = UILabel().then {
$0.numberOfLines = 1
}

private let tooltipView = UIView().then {
$0.backgroundColor = DesignSystem.Color.gray800
$0.layer.cornerRadius = 12
$0.layer.masksToBounds = true
$0.isHidden = true
}

private let tooltipLabel = UILabel().then {
$0.numberOfLines = 0
$0.lineBreakMode = .byWordWrapping
}

// MARK: - Override

public override func setStyle() {
backgroundColor = .clear
contentView.backgroundColor = .clear
selectionStyle = .none
clipsToBounds = false
contentView.clipsToBounds = false
layer.masksToBounds = false
contentView.layer.masksToBounds = false
}

public override func setHierarchy() {
contentView.addSubviews(titleLabel, subtitleLabel, moreButton, middleGuideView)
contentView.addSubviews(titleStack, subtitleLabel, moreButton, refreshStack, tooltipView, middleGuideView)
titleStack.addArrangedSubviews(titleLabel, infoButton)
refreshStack.addArrangedSubviews(refreshIconImageView, refreshLabel)
tooltipView.addSubview(tooltipLabel)
setAction()
}

public override func setLayout() {
titleLabel.snp.makeConstraints {
titleStack.snp.makeConstraints {
$0.top.equalToSuperview()
$0.leading.trailing.equalToSuperview().inset(16)
$0.leading.equalToSuperview().inset(16)
$0.trailing.lessThanOrEqualToSuperview().inset(60)
}

infoButton.snp.makeConstraints {
$0.size.equalTo(20)
}

subtitleLabel.snp.makeConstraints {
$0.top.equalTo(titleLabel.snp.bottom).offset(4)
$0.leading.trailing.equalTo(titleLabel)
$0.top.equalTo(titleStack.snp.bottom).offset(4)
$0.leading.equalToSuperview().inset(16)
$0.trailing.lessThanOrEqualToSuperview().inset(60)
$0.bottom.equalToSuperview().inset(24)
}

middleGuideView.snp.makeConstraints {
$0.top.equalTo(titleLabel.snp.bottom)
$0.top.equalTo(titleStack.snp.bottom)
$0.bottom.equalTo(subtitleLabel.snp.top)
$0.trailing.equalToSuperview()
$0.width.equalTo(1)
}

moreButton.snp.makeConstraints {
$0.centerY.equalTo(middleGuideView.snp.centerY)
$0.trailing.equalToSuperview().inset(12)
}

refreshIconImageView.snp.makeConstraints {
$0.size.equalTo(24)
}

refreshStack.snp.makeConstraints {
$0.top.equalToSuperview()
$0.trailing.equalToSuperview().inset(16)
}

tooltipView.snp.makeConstraints {
$0.top.equalTo(subtitleLabel.snp.bottom).offset(12)
$0.leading.equalToSuperview().inset(16)
$0.trailing.equalToSuperview().inset(13)
}

tooltipLabel.snp.makeConstraints {
$0.top.bottom.equalToSuperview().inset(14)
$0.leading.trailing.equalToSuperview().inset(12)
}
}



public override func prepare() {
titleLabel.attributedText = nil
subtitleLabel.attributedText = nil
refreshLabel.attributedText = nil
tooltipLabel.attributedText = nil
moreButton.isHidden = true
infoButton.isHidden = true
refreshStack.isHidden = true
tooltipView.isHidden = true
layer.zPosition = 0
onTapMore = nil
onTapInfo = nil
onTapRefresh = nil
}

// MARK: - Action

private func setAction() {
moreButton.addTarget(self, action: #selector(didTapMore), for: .touchUpInside)
infoButton.addTarget(self, action: #selector(didTapInfo), for: .touchUpInside)

let refreshTap = UITapGestureRecognizer(target: self, action: #selector(didTapRefresh))
refreshStack.addGestureRecognizer(refreshTap)
}

@objc private func didTapMore() {
onTapMore?()
}


@objc private func didTapInfo() {
onTapInfo?()
}

@objc private func didTapRefresh() {
onTapRefresh?()
}

// MARK: - Configure

public func configure(style: TitleHeaderStyle, title: String, subtitle: String) {

public func configure(
style: TitleHeaderStyle,
title: String,
subtitle: String,
showInfo: Bool = false,
showRefresh: Bool = false,
isRefreshing: Bool = false,
tooltipText: String? = nil
) {
moreButton.isHidden = (style != .more)

infoButton.isHidden = !showInfo
refreshStack.isHidden = !showRefresh

titleLabel.attributedText = .pretendard(.head3_sb_18, text: title)
subtitleLabel.attributedText = .pretendard(.body2_r_14, text: subtitle, color: .flintGray200)
refreshLabel.attributedText = .pretendard(
.micro1_m_10,
text: "업데이트",
color: DesignSystem.Color.secondary400
)

if showRefresh && isRefreshing {
startRefreshAnimationIfNeeded()
} else {
stopRefreshAnimation()
}

let showTooltip = tooltipText != nil
tooltipView.isHidden = !showTooltip
layer.zPosition = showTooltip ? 100 : 0
if let text = tooltipText {
tooltipLabel.attributedText = .pretendard(
.body2_r_14,
text: text,
color: .flintGray300,
lineBreakMode: .byWordWrapping,
lineBreakStrategy: .hangulWordPriority
)
}
}

// MARK: - Refresh Animation

private enum RefreshAnimation {
static let key = "refreshRotation"
static let duration: CFTimeInterval = 1.0
}

private func startRefreshAnimationIfNeeded() {
guard refreshIconImageView.layer.animation(forKey: RefreshAnimation.key) == nil else { return }
let rotation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = 0
rotation.toValue = Double.pi * 2
rotation.duration = RefreshAnimation.duration
rotation.repeatCount = .infinity
rotation.isRemovedOnCompletion = false
refreshIconImageView.layer.add(rotation, forKey: RefreshAnimation.key)
}

private func stopRefreshAnimation() {
refreshIconImageView.layer.removeAnimation(forKey: RefreshAnimation.key)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,21 @@ public final class FlintNavigationBar: BaseView {
case .close:
rightButton.setImage(UIImage(resource: .icCancel), for: .normal)
setPadding(button: rightButton, padding: 12, image: .icCancel)


case .setting:
let image = UIImage(named: "ic_setting", in: .module, with: nil) ?? UIImage()
rightButton.tintColor = .flintWhite
setPadding(button: rightButton, padding: 12, image: image)

case .text(let title, let color):
rightButton.setAttributedTitle(.pretendard(.body1_b_16, text: title, color: color, alignment: .center), for: .normal)
setPadding(button: rightButton, padding: 16, image: nil)


case .icon(let name, let tint):
let image = UIImage(named: name, in: .module, with: nil) ?? UIImage()
rightButton.tintColor = tint
setPadding(button: rightButton, padding: 12, image: image.withRenderingMode(.alwaysTemplate))

case .none:
rightButton.isHidden = true
}
Expand Down
Loading