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
45 changes: 41 additions & 4 deletions FoodDiary/App/Sources/AppFlowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ final class AppFlowController: UIViewController {
private var cancellables = Set<AnyCancellable>()
private let container: DIContainer
private var pendingDeepLinkDate: String?
private var splashView: SplashView?

public init(container: DIContainer) {
self.container = container
Expand All @@ -37,11 +38,19 @@ final class AppFlowController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .sdBase
setupSplashView()
setupNetworkMonitoring()
setupAnalysisCompletionToast()
setupDeepLinkHandling()
}

private func setupSplashView() {
let splash = SplashView()
view.addSubview(splash)
splash.snp.makeConstraints { $0.edges.equalToSuperview() }
splashView = splash
}

private func setupAnalysisCompletionToast() {
NotificationCenter.default.publisher(for: AppNotification.Push.analysisResult)
.receive(on: DispatchQueue.main)
Expand Down Expand Up @@ -85,9 +94,12 @@ extension AppFlowController {

fileprivate func proceedToNextScreen() {
Task {
let isLogin = await validateToken()
prefetchFoodImageAssets()

try? await fetchUserProfile()
async let minimumDelay: Void = Task.sleep(for: .seconds(1.5))
async let loginResult = validateTokenAndFetchProfile()

let (_, isLogin) = try await (minimumDelay, loginResult)

await MainActor.run { [weak self] in
guard let self else { return }
Expand All @@ -98,9 +110,26 @@ extension AppFlowController {
}
}

private func removeSplashView() {
splashView?.animateRemoval()
splashView = nil
}

private func prefetchFoodImageAssets() {
guard let useCase = try? container.resolve(FetchUseCase.self) else { return }
useCase.prefetch(forPreviousWeeks: 2, of: Date())
}

private func validateTokenAndFetchProfile() async -> Bool {
let isLogin = await validateToken()
try? await fetchUserProfile()
return isLogin
}

fileprivate func routeToAppropriateScreen(isLogin: Bool) {
let destinationVC = isLogin ? createMainView() : createLoginView()
transition(to: destinationVC)
removeSplashView()
if isLogin {
registerForRemoteNotificationsAfterLogin()
}
Expand Down Expand Up @@ -209,10 +238,12 @@ extension AppFlowController {
return WeeklyCalendarViewController(
viewModel: weeklyViewModel,
imageProvider: imageProvider,
detailViewControllerFactory: { [weak self] date, records, onDismiss in
detailViewControllerFactory: { [weak self] date, records, scrollToMealType, shouldPopToRoot, onDismiss in
self?.makeDetailViewController(
date: date,
records: records,
scrollToMealType: scrollToMealType,
shouldPopToRoot: shouldPopToRoot,
onDismissWithDate: onDismiss
) ?? UIViewController()
}
Expand All @@ -231,10 +262,12 @@ extension AppFlowController {

return MonthlyCalendarViewController(
viewModel: monthlyViewModel,
detailViewControllerFactory: { [weak self] date, records, onDismiss in
detailViewControllerFactory: { [weak self] date, records, scrollToMealType, shouldPopToRoot, onDismiss in
self?.makeDetailViewController(
date: date,
records: records,
scrollToMealType: scrollToMealType,
shouldPopToRoot: shouldPopToRoot,
onDismissWithDate: onDismiss
) ?? UIViewController()
}
Expand Down Expand Up @@ -441,6 +474,8 @@ extension AppFlowController {
fileprivate func makeDetailViewController(
date: Date,
records: [FoodRecord],
scrollToMealType: MealType? = nil,
shouldPopToRoot: Bool = false,
onDismissWithDate: ((Date) -> Void)? = nil
) -> UIViewController {
guard
Expand All @@ -451,6 +486,8 @@ extension AppFlowController {

return DetailViewController(
viewModel: detailVM,
initialScrollTarget: scrollToMealType,
shouldPopToRoot: shouldPopToRoot,
onDismissWithDate: onDismissWithDate,
editViewControllerFactory: { [weak self] record in
self?.makeEditViewController(for: record) ?? UIViewController()
Expand Down
52 changes: 52 additions & 0 deletions FoodDiary/App/Sources/SplashView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// SplashView.swift
// App
//

import SnapKit
import UIKit

final class SplashView: UIView {
init() {
super.init(frame: .zero)

backgroundColor = UIColor(
red: 0.098, green: 0.094, blue: 0.129, alpha: 1
)

let logoImageView = UIImageView(image: UIImage(named: "logo"))
logoImageView.contentMode = .scaleAspectFit

let characterImageView = UIImageView(image: UIImage(named: "character"))
characterImageView.contentMode = .scaleAspectFit

addSubview(logoImageView)
addSubview(characterImageView)

characterImageView.snp.makeConstraints {
$0.centerX.centerY.equalToSuperview()
$0.width.equalTo(175)
$0.height.equalTo(145)
}

logoImageView.snp.makeConstraints {
$0.centerX.equalToSuperview()
$0.bottom.equalTo(characterImageView.snp.top).offset(-27)
$0.width.equalTo(215)
$0.height.equalTo(64)
}
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func animateRemoval(completion: (() -> Void)? = nil) {
UIView.animate(withDuration: 0.3, animations: {
self.alpha = 0
}, completion: { _ in
self.removeFromSuperview()
completion?()
})
}
}
2 changes: 1 addition & 1 deletion FoodDiary/Data/Sources/DTO/DiaryResponseDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public struct DiaryResponseDTO: Decodable {
public let createdAt: String
public let photos: [DiaryPhotoDTO]

// 새 API 필드 (도메인에서 미사용)
public let note: String?
public let photoCount: Int?
public let userId: String?
Expand Down Expand Up @@ -129,6 +128,7 @@ extension DiaryResponseDTO {
restaurantUrl: restaurantUrl,
address: roadAddress,
hashtags: tags ?? [],
note: note,
createdAt: createdDate,
analysisStatus: status
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "empty-image.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "empty-image 1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "empty-image 2.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.
3 changes: 3 additions & 0 deletions FoodDiary/Domain/Sources/Entity/FoodRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public struct FoodRecord: Identifiable, Equatable, Sendable {
public let restaurantUrl: String?
public let address: String?
public let hashtags: [String]
public let note: String?
public let createdAt: Date
public let analysisStatus: AnalysisStatus

Expand All @@ -55,6 +56,7 @@ public struct FoodRecord: Identifiable, Equatable, Sendable {
restaurantUrl: String? = nil,
address: String? = nil,
hashtags: [String] = [],
note: String? = nil,
createdAt: Date,
analysisStatus: AnalysisStatus = .completed
) {
Expand All @@ -67,6 +69,7 @@ public struct FoodRecord: Identifiable, Equatable, Sendable {
self.restaurantUrl = restaurantUrl
self.address = address
self.hashtags = hashtags
self.note = note
self.createdAt = createdAt
self.analysisStatus = analysisStatus
}
Expand Down
5 changes: 3 additions & 2 deletions FoodDiary/Domain/Sources/UseCase/SaveFoodRecordUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ public struct SaveFoodRecordUseCase<

/// 서버 업로드 수행
/// 분석 완료 시 Remote Push로 결과 수신
@discardableResult
public func execute(
from assets: [any ImageAssetable],
date: Date
) async throws {
) async throws -> [UploadResult] {
let request = CreateFoodRecordRequest(date: date, assets: assets)
_ = try await repository.uploadRecord(request)
return try await repository.uploadRecord(request)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ public final class AddressSearchViewController<
super.viewDidLoad()
setupUI()
setupConstraints()
setupKeyboardDismissGesture()
setupBindings()
}

Expand Down Expand Up @@ -246,6 +247,7 @@ public final class AddressSearchViewController<

Task { @MainActor [weak self] in
guard let self else { return }
self.containerHeightConstraint?.update(offset: self.view.bounds.height)
self.view.layoutIfNeeded()
let contentHeight = self.resultsTableView.contentSize.height
self.containerHeightConstraint?.update(offset: contentHeight)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ final class AddressSearchResultCell: UITableViewCell {

selectButton.snp.makeConstraints {
$0.trailing.equalToSuperview().offset(-Constants.horizontalPadding)
$0.top.equalToSuperview().offset(Constants.verticalPadding)
$0.centerY.equalToSuperview()
}
selectButton.setContentHuggingPriority(.required, for: .horizontal)
selectButton.setContentCompressionResistancePriority(.required, for: .horizontal)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,31 @@ public final class CalendarViewController: UIViewController {
private func showViewController(for mode: ViewMode) {
let targetVC = mode == .weekly ? weeklyVC : monthlyVC

if let currentChild {
currentChild.willMove(toParent: nil)
currentChild.view.removeFromSuperview()
currentChild.removeFromParent()
// 초기 로드: 애니메이션 없이 바로 추가
guard let outgoingVC = currentChild else {
addChild(targetVC)
view.addSubview(targetVC.view)
targetVC.view.snp.makeConstraints { $0.edges.equalToSuperview() }
targetVC.didMove(toParent: self)
currentChild = targetVC
return
}

addChild(targetVC)
targetVC.view.alpha = 0
view.addSubview(targetVC.view)
targetVC.view.snp.makeConstraints { $0.edges.equalToSuperview() }
targetVC.didMove(toParent: self)

currentChild = targetVC
UIView.animate(withDuration: 0.4) {
targetVC.view.alpha = 1
outgoingVC.view.alpha = 0
} completion: { _ in
outgoingVC.view.alpha = 1
outgoingVC.willMove(toParent: nil)
outgoingVC.view.removeFromSuperview()
outgoingVC.removeFromParent()
targetVC.didMove(toParent: self)
self.currentChild = targetVC
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public final class MonthlyCalendarViewController<
// MARK: - Dependencies

private let viewModel: MonthlyCalendarViewModel<RecordRepo, AuthRepo>
private let detailViewControllerFactory: ((Date, [FoodRecord], ((Date) -> Void)?) -> UIViewController)
private let detailViewControllerFactory: ((Date, [FoodRecord], MealType?, Bool, ((Date) -> Void)?) -> UIViewController)

// MARK: - UI Components

Expand Down Expand Up @@ -69,7 +69,7 @@ public final class MonthlyCalendarViewController<

public init(
viewModel: MonthlyCalendarViewModel<RecordRepo, AuthRepo>,
detailViewControllerFactory: @escaping (Date, [FoodRecord], ((Date) -> Void)?) -> UIViewController
detailViewControllerFactory: @escaping (Date, [FoodRecord], MealType?, Bool, ((Date) -> Void)?) -> UIViewController
) {
self.viewModel = viewModel
self.detailViewControllerFactory = detailViewControllerFactory
Expand Down Expand Up @@ -300,7 +300,7 @@ public final class MonthlyCalendarViewController<
// MARK: - Navigation

private func navigateToDetail(date: Date, records: [FoodRecord]) {
let detailVC = detailViewControllerFactory(date, records) { [weak self] date in
let detailVC = detailViewControllerFactory(date, records, nil, false) { [weak self] date in
self?.viewModel.input.send(.updateMonth(date))
}
detailVC.hidesBottomBarWhenPushed = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,16 @@ public final class MonthlyCalendarViewModel<
self.getNicknameUseCase = getNicknameUseCase

let today = Date()
let calendar = Calendar.seoul
let period = calendar.monthlyCalendarPeriod(for: today)
let placeholderDays = Self.generatePlaceholderDays(
for: period, currentMonth: today, calendar: calendar
)

self.stateSubject = CurrentValueSubject(State(currentDisplayDate: today))
state.monthDays = placeholderDays
state.numberOfWeeks = placeholderDays.count / 7
state.monthYearText = today.formatMonthText()

setupBindings()
input.send(.loadNickname)
Expand Down Expand Up @@ -132,6 +141,36 @@ public final class MonthlyCalendarViewModel<
}
}

private static func generatePlaceholderDays(
for period: DateInterval,
currentMonth: Date,
calendar: Calendar
) -> [MonthlyCalendarDay] {
let today = calendar.startOfDay(for: Date())
let currentMonthComponents = calendar.dateComponents([.year, .month], from: currentMonth)

var days: [MonthlyCalendarDay] = []
var currentDate = period.start

while currentDate < period.end {
let dateComponents = calendar.dateComponents([.year, .month], from: currentDate)
let isCurrentMonth = dateComponents.year == currentMonthComponents.year &&
dateComponents.month == currentMonthComponents.month

days.append(MonthlyCalendarDay(
date: currentDate,
dayNumber: calendar.component(.day, from: currentDate),
isCurrentMonth: isCurrentMonth,
isToday: calendar.isDate(currentDate, inSameDayAs: today),
imageURLs: []
))
guard let next = calendar.date(byAdding: .day, value: 1, to: currentDate) else { break }
currentDate = next
}

return days
}

private func requestPhotoAuthorizationIfNeeded() async {
let status = requestPhotoAuthorizationUseCase.currentStatus()
if status == .notDetermined {
Expand Down
Loading
Loading