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 Projects/App/Sources/AppFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ struct AppFeature {
case .intro(.delegate(.loginSucceeded)):
state.route = .mainTab
return .none

case .mainTab(.delegate(.needsAuthentication)):
state.route = .intro
return .none

case .splash, .intro, .mainTab:
return .none
Expand Down
5 changes: 5 additions & 0 deletions Projects/App/Sources/DoriApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ComposableArchitecture
import DoriDesignSystem
import DoriNetwork
import DoriNetworkImpl
import FeatureMyPage
import FeatureOnboarding
import PlatformKakaoAuth
import PlatformKeychain
Expand Down Expand Up @@ -42,6 +43,10 @@ struct DoriApp: App {
networkService: networkService,
tokenStore: tokenStore
)
$0.myPageAPIClient = .live(
networkService: networkService,
tokenStore: tokenStore
)
}

FontManager.registerAllFonts()
Expand Down
16 changes: 15 additions & 1 deletion Projects/App/Sources/MainTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ struct MainTabFeature {
case calendar(CalendarFeature.Action)
case history(HistoryFeature.Action)
case myPage(MyPageFeature.Action)
case delegate(Delegate)

enum Delegate: Equatable {
case needsAuthentication
}
}

var body: some ReducerOf<Self> {
Expand All @@ -48,7 +53,16 @@ struct MainTabFeature {
state.selectedTab = tab
return .none

case .calendar, .history, .myPage:
case .myPage(.delegate(.didLogout)):
return .send(.delegate(.needsAuthentication))

case .myPage(.delegate(.didWithdraw)):
return .send(.delegate(.needsAuthentication))

case .myPage(.delegate(.authExpired)):
return .send(.delegate(.needsAuthentication))

case .calendar, .history, .myPage, .delegate:
return .none
}
}
Expand Down
41 changes: 41 additions & 0 deletions Projects/Core/DoriDesignSystem/Sources/AlertButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// AlertButton.swift
// DoriDesignSystem
//
// Created by 강동영 on 2/13/26.
//

public struct AlertButton {
public let title: String
public let action: @MainActor () -> Void

public init(
title: String,
action: @escaping @MainActor () -> Void
) {
self.title = title
self.action = action
}

public init(
_ type: AlertButtonType,
action: @escaping @MainActor () -> Void
) {
self.title = type.title
self.action = action
}

public enum AlertButtonType {
case yes
case no

public var title: String {
switch self {
case .yes:
return "예"
case .no:
return "아니오"
}
}
}
}
112 changes: 112 additions & 0 deletions Projects/Core/DoriDesignSystem/Sources/DoriCommonAlert.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// DoriCommonAlert.swift
// DoriDesignSystem
//
// Created by 강동영 on 2/13/26.
//

import SwiftUI

public struct DoriCommonAlert: View {
@Binding var isPresented: Bool

private let title: String
private let description: String?
private let secondaryButton: AlertButton?
private let primaryButton: AlertButton

public init(
isPresented: Binding<Bool>,
title: String,
description: String? = nil,
secondaryButton: AlertButton?,
primaryButton: AlertButton
) {
self._isPresented = isPresented
self.title = title
self.description = description
self.secondaryButton = secondaryButton
self.primaryButton = primaryButton
}

public var body: some View {
ZStack {
Color.black.opacity(0.4)
.ignoresSafeArea()
.onTapGesture {
isPresented = false
secondaryButton?.action()
}

VStack(spacing: 0) {
contentArea
buttonArea
}
.padding(16)
.background(.doriWhite)
.cornerRadius(10)
.padding(.horizontal, 24)
.scaleEffect(isPresented ? 1.0 : 0.8)
.opacity(isPresented ? 1.0 : 0.0)
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: isPresented)
}
}

var contentArea: some View {
VStack(alignment: .center, spacing: 8) {
Text(title)
.pretendard(.headline(.h1))
.foregroundStyle(.doriBlack)

if let description = description {
Text(description)
.pretendard(.body(.r4))
.foregroundStyle(.grey600)
.multilineTextAlignment(.center)
}
}
.padding(.vertical, 40)
}

var buttonArea: some View {
HStack(spacing: 10) {
if let secondaryButton = secondaryButton {
PrimaryButton(title: secondaryButton.title) {
secondaryButton.action()
}
.backgroundColor(.grey100)
.foregroundColor(.black)
}

PrimaryButton(title: primaryButton.title) {
primaryButton.action()
}
}
.frame(maxHeight: 56)
}
}

#Preview {
DoriCommonAlert(
isPresented: .constant(true),
title: "로그아웃 하시겠습니까?",
secondaryButton: AlertButton(.no) {
print("no")
},
primaryButton: AlertButton(.yes) {
print("yes")
}
)
}

#Preview {
DoriCommonAlert(
isPresented: .constant(true),
title: "회원탈퇴",
description: "정말 도리를 탈퇴하실건가요?\n재가입 시에도 이용 내역은 복구되지 않습니다.",
secondaryButton: AlertButton(.no) {
},
primaryButton: AlertButton(.yes) {
}
)
}
42 changes: 42 additions & 0 deletions Projects/Core/DoriDesignSystem/Sources/DoriToast.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// DoriToast.swift
// DoriDesignSystem
//
// Created by 강동영 on 2/13/26.
//

import Foundation

public struct DoriToast: Equatable, Sendable {
public let id: UUID
public let type: ToastType
public let message: String
public let duration: TimeInterval

public init(
id: UUID = UUID(),
type: ToastType,
message: String,
duration: TimeInterval? = nil
) {
self.id = id
self.type = type
self.message = message
self.duration = duration ?? type.defaultDuration
}
}

public enum ToastType: Equatable, Sendable {
case success
case error
case info

public var defaultDuration: TimeInterval {
switch self {
case .success, .info:
return 2.0
case .error:
return 3.0
}
}
}
62 changes: 62 additions & 0 deletions Projects/Core/DoriDesignSystem/Sources/DoriToastModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// DoriToastModifier.swift
// DoriDesignSystem
//
// Created by 강동영 on 2/13/26.
//

import SwiftUI

struct DoriToastModifier: ViewModifier {
let toast: DoriToast?
let alignment: Alignment
let onDismiss: @MainActor () -> Void

func body(content: Content) -> some View {
content
.overlay(alignment: alignment) {
Group {
if let toast {
DoriToastView(toast: toast)
.id(toast.id)
.transition(
.move(edge: alignment == .top ? .top : .bottom)
.combined(with: .opacity)
)
.padding(
alignment == .top ? .top : .bottom,
8
)
.task(id: toast.id) {
try? await Task.sleep(for: .seconds(toast.duration))
guard !Task.isCancelled else { return }
onDismiss()
}
}
}
.animation(
.spring(
response: 0.35,
dampingFraction: 0.8
),
value: toast
)
}
}
}

public extension View {
func doriToast(
_ toast: DoriToast?,
alignment: Alignment = .bottom,
onDismiss: @escaping @MainActor () -> Void
) -> some View {
modifier(
DoriToastModifier(
toast: toast,
alignment: alignment,
onDismiss: onDismiss
)
)
}
}
Loading