diff --git a/.gitignore b/.gitignore
index 0ea58a5..90e4c2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -121,3 +121,5 @@ Derived/
*.p12
master.key
*.package.resolved
+
+GoogleService-Info.plist
\ No newline at end of file
diff --git a/Plugins/ModulePlugin/ProjectDescriptionHelpers/Taget+Extensions/TargetDependency+Module.swift b/Plugins/ModulePlugin/ProjectDescriptionHelpers/Taget+Extensions/TargetDependency+Module.swift
index 07275d7..79e4aa4 100644
--- a/Plugins/ModulePlugin/ProjectDescriptionHelpers/Taget+Extensions/TargetDependency+Module.swift
+++ b/Plugins/ModulePlugin/ProjectDescriptionHelpers/Taget+Extensions/TargetDependency+Module.swift
@@ -119,7 +119,8 @@ public extension [TargetDependency] {
]
case .Folio:
return [
- .external(name: "GoogleMobileAds")
+ .external(name: "GoogleMobileAds"),
+ .external(name: "FirebaseAnalytics")
]
default: return []
}
diff --git a/Plugins/ModulePlugin/ProjectDescriptionHelpers/Target+Module.swift b/Plugins/ModulePlugin/ProjectDescriptionHelpers/Target+Module.swift
index 16eaf13..3858a8f 100644
--- a/Plugins/ModulePlugin/ProjectDescriptionHelpers/Target+Module.swift
+++ b/Plugins/ModulePlugin/ProjectDescriptionHelpers/Target+Module.swift
@@ -309,7 +309,7 @@ public extension Target {
deploymentTarget: .shared(product),
infoPlist: .shared(product),
sources: .path(type: .implement),
- resources: nil,
+ resources: .path(type: .implement),
copyFiles: nil,
headers: nil,
entitlements: nil,
@@ -341,7 +341,7 @@ public extension Target {
deploymentTarget: .shared(product, module: module),
infoPlist: .shared(product, module: module),
sources: .path(type: type),
- resources: nil,
+ resources: .path(type: .implement),
copyFiles: nil,
headers: nil,
entitlements: nil,
diff --git a/Projects/Dying/.package.resolved b/Projects/Dying/.package.resolved
deleted file mode 100644
index 03774b2..0000000
--- a/Projects/Dying/.package.resolved
+++ /dev/null
@@ -1,104 +0,0 @@
-{
- "pins" : [
- {
- "identity" : "combine-schedulers",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/combine-schedulers",
- "state" : {
- "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swift-case-paths",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-case-paths",
- "state" : {
- "revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swift-clocks",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-clocks",
- "state" : {
- "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swift-collections",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/apple/swift-collections",
- "state" : {
- "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
- "version" : "1.0.4"
- }
- },
- {
- "identity" : "swift-composable-architecture",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-composable-architecture",
- "state" : {
- "revision" : "195284b94b799b326729640453f547f08892293a",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swift-concurrency-extras",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-concurrency-extras",
- "state" : {
- "revision" : "ea631ce892687f5432a833312292b80db238186a",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swift-custom-dump",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-custom-dump",
- "state" : {
- "revision" : "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swift-dependencies",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-dependencies",
- "state" : {
- "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swift-identified-collections",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-identified-collections",
- "state" : {
- "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swiftui-navigation",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swiftui-navigation",
- "state" : {
- "revision" : "f5bcdac5b6bb3f826916b14705f37a3937c2fd34",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "xctest-dynamic-overlay",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
- "state" : {
- "revision" : "302891700c7fa3b92ebde9fe7b42933f8349f3c7",
- "version" : "1.0.0"
- }
- }
- ],
- "version" : 2
-}
diff --git a/Projects/Folio/.package.resolved b/Projects/Folio/.package.resolved
deleted file mode 100644
index 27d02ec..0000000
--- a/Projects/Folio/.package.resolved
+++ /dev/null
@@ -1,158 +0,0 @@
-{
- "pins" : [
- {
- "identity" : "combine-schedulers",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/combine-schedulers",
- "state" : {
- "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "googleappmeasurement",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/google/GoogleAppMeasurement.git",
- "state" : {
- "revision" : "03b9beee1a61f62d32c521e172e192a1663a5e8b",
- "version" : "10.13.0"
- }
- },
- {
- "identity" : "googleutilities",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/google/GoogleUtilities.git",
- "state" : {
- "revision" : "c38ce365d77b04a9a300c31061c5227589e5597b",
- "version" : "7.11.5"
- }
- },
- {
- "identity" : "nanopb",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/firebase/nanopb.git",
- "state" : {
- "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692",
- "version" : "2.30909.0"
- }
- },
- {
- "identity" : "promises",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/google/promises.git",
- "state" : {
- "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e",
- "version" : "2.3.1"
- }
- },
- {
- "identity" : "swift-case-paths",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-case-paths",
- "state" : {
- "revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swift-clocks",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-clocks",
- "state" : {
- "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swift-collections",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/apple/swift-collections",
- "state" : {
- "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
- "version" : "1.0.4"
- }
- },
- {
- "identity" : "swift-composable-architecture",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-composable-architecture",
- "state" : {
- "revision" : "a7c1f799b55ecb418f85094b142565834f7ee7c7",
- "version" : "1.2.0"
- }
- },
- {
- "identity" : "swift-concurrency-extras",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-concurrency-extras",
- "state" : {
- "revision" : "ea631ce892687f5432a833312292b80db238186a",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swift-custom-dump",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-custom-dump",
- "state" : {
- "revision" : "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swift-dependencies",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-dependencies",
- "state" : {
- "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swift-identified-collections",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swift-identified-collections",
- "state" : {
- "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "swift-package-manager-google-mobile-ads",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/googleads/swift-package-manager-google-mobile-ads",
- "state" : {
- "revision" : "1a6faf6b9b82ddf8780f678745381b8628711077",
- "version" : "10.9.0"
- }
- },
- {
- "identity" : "swift-package-manager-google-user-messaging-platform",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/googleads/swift-package-manager-google-user-messaging-platform.git",
- "state" : {
- "revision" : "129fa838520cd02174f890ae0cfe0242e60714ae",
- "version" : "2.1.0"
- }
- },
- {
- "identity" : "swiftui-navigation",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/swiftui-navigation",
- "state" : {
- "revision" : "f5bcdac5b6bb3f826916b14705f37a3937c2fd34",
- "version" : "1.0.0"
- }
- },
- {
- "identity" : "xctest-dynamic-overlay",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
- "state" : {
- "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631",
- "version" : "1.0.2"
- }
- }
- ],
- "version" : 2
-}
diff --git a/Projects/Folio/Shared/DesignSystem/Sources/Color+Extension.swift b/Projects/Folio/Shared/DesignSystem/Sources/Color+Extension.swift
index 3bbeee9..d7f9c63 100644
--- a/Projects/Folio/Shared/DesignSystem/Sources/Color+Extension.swift
+++ b/Projects/Folio/Shared/DesignSystem/Sources/Color+Extension.swift
@@ -8,7 +8,61 @@
import SwiftUI
public extension Color {
- static func blackOrWhite(_ isSelected: Bool = false) -> Self {
- return isSelected ? Color(uiColor: .label) : Color(uiColor: .systemBackground)
+ init(hex: String) {
+ let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
+ var int: UInt64 = 0
+ Scanner(string: hex).scanHexInt64(&int)
+ let a, r, g, b: UInt64
+ switch hex.count {
+ case 3: // RGB (12-bit)
+ (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
+ case 6: // RGB (24-bit)
+ (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
+ case 8: // ARGB (32-bit)
+ (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
+ default:
+ (a, r, g, b) = (1, 1, 1, 0)
+ }
+
+ self.init(
+ .sRGB,
+ red: Double(r) / 255,
+ green: Double(g) / 255,
+ blue: Double(b) / 255,
+ opacity: Double(a) / 255
+ )
+ }
+
+ static var foreground: Self {
+ return Color(uiColor: .label)
+ }
+
+ static var background: Self {
+ return Color(uiColor: .systemBackground)
+ }
+
+// static func blackOrWhite(_ isSelected: Bool = false) -> Self {
+// return isSelected ? Color(uiColor: .label) : Color(uiColor: .systemBackground)
+// }
+
+ func toHex() -> String {
+ let uic = UIColor(self)
+ guard let components = uic.cgColor.components, components.count >= 3 else {
+ return ""
+ }
+ let r = Float(components[0])
+ let g = Float(components[1])
+ let b = Float(components[2])
+ var a = Float(1.0)
+
+ if components.count >= 4 {
+ a = Float(components[3])
+ }
+
+ if a != Float(1.0) {
+ return String(format: "%02lX%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255), lroundf(a * 255))
+ } else {
+ return String(format: "%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255))
+ }
}
}
diff --git a/Projects/Folio/Shared/DesignSystem/Sources/MinimalButton.swift b/Projects/Folio/Shared/DesignSystem/Sources/MinimalButton.swift
index 57471eb..af11440 100644
--- a/Projects/Folio/Shared/DesignSystem/Sources/MinimalButton.swift
+++ b/Projects/Folio/Shared/DesignSystem/Sources/MinimalButton.swift
@@ -40,7 +40,7 @@ public struct MinimalButton: View {
}
.padding(.vertical, 10)
})
- .background(.black)
+ .background(isActive ? Color.foreground : Color.foreground) //TODO: active 현재 미사용
.clipShape(
RoundedRectangle(
cornerRadius: 8,
diff --git a/Projects/Toolinder/.package.resolved b/Projects/Toolinder/.package.resolved
deleted file mode 100644
index 6963374..0000000
--- a/Projects/Toolinder/.package.resolved
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "pins" : [
- {
- "identity" : "realm-core",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/realm/realm-core",
- "state" : {
- "revision" : "c04f5e401a1ec682e6b08b1ee157e19a0f834a5f",
- "version" : "13.17.1"
- }
- },
- {
- "identity" : "realm-swift",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/realm/realm-swift",
- "state" : {
- "revision" : "330a239712af77a3b0926b9ffa9582302a0b9923",
- "version" : "10.42.1"
- }
- }
- ],
- "version" : 2
-}
diff --git a/Projects/Toolinder/App/Resources/Localizable.xcstrings b/Projects/Toolinder/App/Resources/Localizable.xcstrings
new file mode 100644
index 0000000..62ecc38
--- /dev/null
+++ b/Projects/Toolinder/App/Resources/Localizable.xcstrings
@@ -0,0 +1,142 @@
+{
+ "sourceLanguage" : "en",
+ "strings" : {
+ "Buy" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "売り"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "매수"
+ }
+ }
+ }
+ },
+ "Edit" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "編集"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "편집"
+ }
+ }
+ }
+ },
+ "History" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "記録"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "기록"
+ }
+ }
+ }
+ },
+ "Save" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "保存"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "저장"
+ }
+ }
+ }
+ },
+ "Sell" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "買い"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "매도"
+ }
+ }
+ }
+ },
+ "Summary" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "概略"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "요약"
+ }
+ }
+ }
+ },
+ "Ticker" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "銘柄"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "종목"
+ }
+ }
+ }
+ },
+ "Trade" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "商売"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "거래"
+ }
+ }
+ }
+ }
+ },
+ "version" : "1.0"
+}
\ No newline at end of file
diff --git a/Projects/Toolinder/App/Sources/AppDelegate.swift b/Projects/Toolinder/App/Sources/AppDelegate.swift
index 7614bed..508b59d 100644
--- a/Projects/Toolinder/App/Sources/AppDelegate.swift
+++ b/Projects/Toolinder/App/Sources/AppDelegate.swift
@@ -7,6 +7,8 @@
//
import UIKit
+import SwiftUI
+import FirebaseCore
import GoogleMobileAds
@@ -22,7 +24,8 @@ class AppDelegate: NSObject, UIApplicationDelegate {
GADMobileAds.sharedInstance().start(completionHandler: nil)
}
}
-
+
+ FirebaseApp.configure()
return true
}
diff --git a/Projects/Toolinder/App/Sources/RootApp.swift b/Projects/Toolinder/App/Sources/RootApp.swift
index 169eb73..ef2b685 100644
--- a/Projects/Toolinder/App/Sources/RootApp.swift
+++ b/Projects/Toolinder/App/Sources/RootApp.swift
@@ -15,15 +15,8 @@ import ToolinderDomain
@main
struct RootApp: App {
- let modelContainer: ModelContainer
+ @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
- init() {
- do {
- modelContainer = try ModelContainer(for: Ticker.self, Trade.self)
- } catch {
- fatalError("Could not initialize ModelContainer \(error)")
- }
- }
var body: some Scene {
WindowGroup {
RootView(
@@ -32,8 +25,12 @@ struct RootApp: App {
._printChanges()
}
)
+ .modelContainer(for: [
+ Ticker.self,
+ Trade.self,
+ Tag.self
+ ])
.onAppear(perform: UIApplication.shared.hideKeyboard)
}
- .modelContainer(modelContainer)
}
}
diff --git a/Projects/Toolinder/App/Support/ToolinderAppIOS-Info.plist b/Projects/Toolinder/App/Support/ToolinderAppIOS-Info.plist
index 16f5986..e4e5ae3 100644
--- a/Projects/Toolinder/App/Support/ToolinderAppIOS-Info.plist
+++ b/Projects/Toolinder/App/Support/ToolinderAppIOS-Info.plist
@@ -7,7 +7,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- Toolinder
+ Toff
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
diff --git a/Projects/Toolinder/App/ToolinderIOS.entitlements b/Projects/Toolinder/App/ToolinderIOS.entitlements
index 90bda52..9c1f471 100644
--- a/Projects/Toolinder/App/ToolinderIOS.entitlements
+++ b/Projects/Toolinder/App/ToolinderIOS.entitlements
@@ -2,17 +2,17 @@
- com.apple.security.app-sandbox
-
aps-environment
development
com.apple.developer.icloud-container-identifiers
- iCloud.toolinder
+ iCloud.com.tamsadan.toolinder
com.apple.developer.icloud-services
CloudKit
+ com.apple.security.app-sandbox
+
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TagDTO.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TagDTO.swift
new file mode 100644
index 0000000..de18175
--- /dev/null
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TagDTO.swift
@@ -0,0 +1,34 @@
+//
+// TagDTO.swift
+// ToolinderDomainTradeInterface
+//
+// Created by 송영모 on 2023/10/01.
+//
+
+import Foundation
+import SwiftData
+import SwiftUI
+
+public class TagDTO {
+ public var id: UUID = UUID()
+ public var hex: String = ""
+ public var name: String = ""
+
+ public init(
+ id: UUID = .init(),
+ hex: String,
+ name: String
+ ) {
+ self.id = id
+ self.hex = hex
+ self.name = name
+ }
+
+ func toDomain() -> Tag {
+ return Tag(
+ id: id,
+ hex: hex,
+ name: name
+ )
+ }
+}
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TickerDTO.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TickerDTO.swift
index 96a995b..1f7b2fd 100644
--- a/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TickerDTO.swift
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TickerDTO.swift
@@ -9,25 +9,33 @@ import Foundation
import SwiftData
public class TickerDTO {
+ public let id: UUID
public var type: TickerType
public var currency: Currency
public var name: String
+ public var tags: [Tag]
public init(
+ id: UUID = .init(),
type: TickerType,
currency: Currency,
- name: String
+ name: String,
+ tags: [Tag]
) {
+ self.id = id
self.type = type
self.currency = currency
self.name = name
+ self.tags = tags
}
func toDomain() -> Ticker {
return Ticker(
+ id: id,
type: type,
currency: currency,
- name: name
+ name: name,
+ tags: tags
)
}
}
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TradeDTO.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TradeDTO.swift
index 8ea8e65..d4b9f9e 100644
--- a/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TradeDTO.swift
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TradeDTO.swift
@@ -9,9 +9,10 @@ import Foundation
import SwiftData
public struct TradeDTO {
+ public let id: UUID
public var side: TradeSide
public var price: Double
- public var volume: Double
+ public var quantity: Double
public var fee: Double
public var images: [Data]
public var note: String
@@ -20,19 +21,21 @@ public struct TradeDTO {
public var ticker: Ticker?
public init(
+ id: UUID = .init(),
side: TradeSide = .buy,
price: Double = 0,
- volume: Double = 0,
+ quantity: Double = 0,
fee: Double = 0,
images: [Data] = [],
note: String = "",
date: Date = .now,
ticker: Ticker
) {
+ self.id = id
self.side = side
self.images = images
self.price = price
- self.volume = volume
+ self.quantity = quantity
self.fee = fee
self.note = note
self.date = date
@@ -41,9 +44,10 @@ public struct TradeDTO {
func toDomain() -> Trade {
return Trade(
+ id: id,
side: side,
price: price,
- volume: volume,
+ quantity: quantity,
fee: fee,
images: images,
note: note,
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerSummaryDataEntity.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerSummaryDataEntity.swift
index 9cc31ed..f9343d4 100644
--- a/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerSummaryDataEntity.swift
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerSummaryDataEntity.swift
@@ -31,12 +31,12 @@ public extension Ticker {
for trade in trades {
switch trade.side {
case .buy:
- totalAmount += trade.price * trade.volume
- buyVolume += trade.volume
+ totalAmount += trade.price * trade.quantity
+ buyVolume += trade.quantity
case .sell:
- totalAmount -= avgPrice * trade.volume
- sellVoume += trade.volume
+ totalAmount -= avgPrice * trade.quantity
+ sellVoume += trade.quantity
}
totalVolume = buyVolume - sellVoume
@@ -47,7 +47,7 @@ public extension Ticker {
// 수익 계산
if trade.side == .sell {
- profit += (trade.price - avgPrice) * trade.volume
+ profit += (trade.price - avgPrice) * trade.quantity
}
}
// 수익률 계산
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerTypeChartDataEntity.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerTypeChartDataEntity.swift
index f45e408..60aaba1 100644
--- a/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerTypeChartDataEntity.swift
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerTypeChartDataEntity.swift
@@ -19,7 +19,7 @@ public extension [Trade] {
return TickerType.allCases.map { type in
let trades = self.filter({ $0.ticker?.type == type })
let hold = trades.map { trade in
- let sum: Double = trade.price * trade.volume
+ let sum: Double = trade.price * trade.quantity
let sign: Double = trade.side == .buy ? 1.0 : -1.0
return sum * sign
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Model/TradeSide.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Model/TradeSide.swift
index d159c4c..dd4a1e7 100644
--- a/Projects/Toolinder/Domain/Trade/Interface/Sources/Model/TradeSide.swift
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Model/TradeSide.swift
@@ -8,6 +8,6 @@
import Foundation
public enum TradeSide: String, Codable, CaseIterable {
- case buy
- case sell
+ case buy = "Buy"
+ case sell = "Sell"
}
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Tag.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Tag.swift
new file mode 100644
index 0000000..eb98edb
--- /dev/null
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Tag.swift
@@ -0,0 +1,29 @@
+//
+// HashTag.swift
+// ToolinderDomainTradeInterface
+//
+// Created by 송영모 on 2023/09/30.
+//
+
+import Foundation
+import SwiftData
+import SwiftUI
+
+@Model
+public class Tag {
+ public let id: UUID = UUID()
+ public var hex: String = ""
+ public var name: String = ""
+
+ @Relationship public var tickers: [Ticker]? = []
+
+ public init(
+ id: UUID = .init(),
+ hex: String,
+ name: String
+ ) {
+ self.id = id
+ self.hex = hex
+ self.name = name
+ }
+}
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Ticker.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Ticker.swift
index 5475a9b..23f2897 100644
--- a/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Ticker.swift
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Ticker.swift
@@ -10,19 +10,25 @@ import SwiftData
@Model
public class Ticker {
+ public let id: UUID = UUID()
public var type: TickerType = TickerType.stock
public var currency: Currency = Currency.dollar
public var name: String = ""
@Relationship(deleteRule: .cascade, inverse: \Trade.ticker) public var trades: [Trade]? = []
+ @Relationship(inverse: \Tag.tickers) public var tags: [Tag]? = []
public init(
+ id: UUID = .init(),
type: TickerType,
currency: Currency,
- name: String
+ name: String,
+ tags: [Tag]
) {
+ self.id = id
self.type = type
self.currency = currency
self.name = name
+ self.tags = tags
}
}
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Trade.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Trade.swift
index e91b9aa..8257fc1 100644
--- a/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Trade.swift
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Trade.swift
@@ -10,9 +10,10 @@ import SwiftData
@Model
public class Trade {
+ public let id: UUID = UUID()
public var side: TradeSide = TradeSide.buy
public var price: Double = 0
- public var volume: Double = 0
+ public var quantity: Double = 0
public var fee: Double = 0
public var images: [Data] = []
public var note: String = ""
@@ -21,19 +22,21 @@ public class Trade {
@Relationship public var ticker: Ticker?
public init(
+ id: UUID = .init(),
side: TradeSide,
price: Double,
- volume: Double,
+ quantity: Double,
fee: Double,
images: [Data],
note: String,
date: Date,
ticker: Ticker?
) {
+ self.id = id
self.side = side
self.images = images
self.price = price
- self.volume = volume
+ self.quantity = quantity
self.fee = fee
self.note = note
self.date = date
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/StorageContainer.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/StorageContainer.swift
index a2706db..812ff4e 100644
--- a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/StorageContainer.swift
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/StorageContainer.swift
@@ -16,8 +16,7 @@ public class StorageContainer {
private init() {
do {
- container = try ModelContainer(for: Ticker.self, Trade.self)
-
+ container = try ModelContainer(for: Ticker.self, Trade.self, Tag.self)
if let container {
context = ModelContext(container)
}
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TagRepository.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TagRepository.swift
new file mode 100644
index 0000000..5530a0d
--- /dev/null
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TagRepository.swift
@@ -0,0 +1,76 @@
+//
+// TagRepository.swift
+// ToolinderDomainTradeInterface
+//
+// Created by 송영모 on 2023/10/01.
+//
+
+import Foundation
+import SwiftData
+
+public protocol TagRepositoryInterface {
+ func fetchTags(descriptor: FetchDescriptor) -> Result<[Tag], TagError>
+ func saveTag(_ tag: TagDTO) -> Result
+ func updateTag(_ tag: Tag, new newTag: TagDTO) -> Result
+ func deleteTag(_ tag: Tag) -> Result
+
+ func isValidatedSaveTag(_ tag: TagDTO) -> Bool
+ func isValidatedUpdateTag(_ tag: Tag, new newTag: TagDTO) -> Bool
+ func isValidatedDeleteTag(_ tag: Tag) -> Bool
+}
+
+public class TagRepository: TagRepositoryInterface {
+ private var context: ModelContext? = StorageContainer.shared.context
+
+ public init() { }
+
+ public func fetchTags(descriptor: FetchDescriptor) -> Result<[Tag], TagError> {
+ if let tags = try? context?.fetch(descriptor) {
+ return .success(tags)
+ } else {
+ return .failure(.unknown)
+ }
+ }
+
+ public func saveTag(_ tag: TagDTO) -> Result {
+ if isValidatedSaveTag(tag) {
+ let tag = tag.toDomain()
+ context?.insert(tag)
+ return .success(tag)
+ } else {
+ return .failure(.unknown)
+ }
+ }
+
+ public func updateTag(_ tag: Tag, new newTag: TagDTO) -> Result {
+ if isValidatedUpdateTag(tag, new: newTag) {
+ let tag = tag
+ tag.hex = newTag.hex
+ tag.name = newTag.name
+ return .success(tag)
+ } else {
+ return .failure(.unknown)
+ }
+ }
+
+ public func deleteTag(_ tag: Tag) -> Result {
+ if isValidatedDeleteTag(tag) {
+ context?.delete(tag)
+ return .success(tag)
+ } else {
+ return .failure(.unknown)
+ }
+ }
+
+ public func isValidatedSaveTag(_ tag: TagDTO) -> Bool {
+ return true
+ }
+
+ public func isValidatedUpdateTag(_ tag: Tag, new newTag: TagDTO) -> Bool {
+ return true
+ }
+
+ public func isValidatedDeleteTag(_ tag: Tag) -> Bool {
+ return true
+ }
+}
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TickerRepository.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TickerRepository.swift
index 3471bdf..d29209a 100644
--- a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TickerRepository.swift
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TickerRepository.swift
@@ -46,6 +46,7 @@ public class TickerRepository: TickerRepositoryInterface {
ticker.type = newTicker.type
ticker.currency = newTicker.currency
ticker.name = newTicker.name
+ ticker.tags = newTicker.tags
return .success(ticker)
} else {
return .failure(.unknown)
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TradeRepository.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TradeRepository.swift
index 002d086..b34b012 100644
--- a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TradeRepository.swift
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TradeRepository.swift
@@ -10,13 +10,13 @@ import SwiftData
public protocol TradeRepositoryInterface {
func fetchTrades(descriptor: FetchDescriptor) -> Result<[Trade], TradeError>
- func saveTrade(dto: TradeDTO) -> Result
- func updateTrade(model: Trade, dto: TradeDTO) -> Result
- func deleteTrade(trade: Trade) -> Result
+ func saveTrade(_ trade: TradeDTO) -> Result
+ func updateTrade(_ trade: Trade, new newTrade: TradeDTO) -> Result
+ func deleteTrade(_ trade: Trade) -> Result
- func isValidatedSaveTrade(dto: TradeDTO) -> Bool
- func isValidatedUpdateTrade(origin: Trade, new: TradeDTO) -> Bool
- func isValidatedDeleteTrade(origin: Trade) -> Bool
+ func isValidatedSaveTrade(_ trade: TradeDTO) -> Bool
+ func isValidatedUpdateTrade(_ trade: Trade, new newTrade: TradeDTO) -> Bool
+ func isValidatedDeleteTrade(_ trade: Trade) -> Bool
}
public class TradeRepository: TradeRepositoryInterface {
@@ -24,44 +24,6 @@ public class TradeRepository: TradeRepositoryInterface {
public init() { }
- public func fetchTickers(descriptor: FetchDescriptor) -> Result<[Ticker], TradeError> {
- if let tickers = try? context?.fetch(descriptor) {
- return .success(tickers)
- } else {
- return .failure(.unknown)
- }
- }
-
- public func saveTicker(ticker: Ticker) -> Result {
- if isValidatedSaveTicker(new: ticker) {
- context?.insert(ticker)
- return .success(ticker)
- } else {
- return .failure(.unknown)
- }
- }
-
- public func updateTicker(model: Ticker, dto: TickerDTO) -> Result {
- if isValidatedUpdateTicker(origin: model, new: dto) {
- let ticker = model
- ticker.type = dto.type
- ticker.currency = dto.currency
- ticker.name = dto.name
- return .success(ticker)
- } else {
- return .failure(.unknown)
- }
- }
-
- public func deleteTicker(ticker: Ticker) -> Result {
- if isValidatedDeleteTicker(origin: ticker) {
- context?.delete(ticker)
- return .success(ticker)
- } else {
- return .failure(.unknown)
- }
- }
-
public func fetchTrades(descriptor: FetchDescriptor) -> Result<[Trade], TradeError> {
if let trades = try? context?.fetch(descriptor) {
return .success(trades)
@@ -70,9 +32,9 @@ public class TradeRepository: TradeRepositoryInterface {
}
}
- public func saveTrade(dto: TradeDTO) -> Result {
- if isValidatedSaveTrade(dto: dto) {
- let trade = dto.toDomain()
+ public func saveTrade(_ trade: TradeDTO) -> Result {
+ if isValidatedSaveTrade(trade) {
+ let trade = trade.toDomain()
context?.insert(trade)
return .success(trade)
} else {
@@ -80,24 +42,24 @@ public class TradeRepository: TradeRepositoryInterface {
}
}
- public func updateTrade(model: Trade, dto: TradeDTO) -> Result {
- if isValidatedUpdateTrade(origin: model, new: dto) {
- let trade = model
- trade.ticker = dto.ticker
- trade.date = dto.date
- trade.side = dto.side
- trade.price = dto.price
- trade.volume = dto.volume
- trade.images = dto.images
- trade.note = dto.note
+ public func updateTrade(_ trade: Trade, new newTrade: TradeDTO) -> Result {
+ if isValidatedUpdateTrade(trade, new: newTrade) {
+ let trade = trade
+ trade.ticker = newTrade.ticker
+ trade.date = newTrade.date
+ trade.side = newTrade.side
+ trade.price = newTrade.price
+ trade.quantity = newTrade.quantity
+ trade.images = newTrade.images
+ trade.note = newTrade.note
return .success(trade)
} else {
return .failure(.unknown)
}
}
- public func deleteTrade(trade: Trade) -> Result {
- if isValidatedDeleteTrade(origin: trade) {
+ public func deleteTrade(_ trade: Trade) -> Result {
+ if isValidatedDeleteTrade(trade) {
context?.delete(trade)
return .success(trade)
} else {
@@ -105,79 +67,60 @@ public class TradeRepository: TradeRepositoryInterface {
}
}
- public func isValidatedSaveTicker(new: Ticker) -> Bool {
- return true
- }
-
- public func isValidatedUpdateTicker(origin: Ticker, new: TickerDTO) -> Bool {
- if new.type != nil && new.currency != nil && new.name.isEmpty == false {
- return true
- }
- return false
- }
-
- public func isValidatedDeleteTicker(origin: Ticker) -> Bool {
- return true
- }
-
- public func isValidatedSaveTrade(dto: TradeDTO) -> Bool {
- if dto.side == .buy { return true }
-
- guard let trades = try? fetchTrades(descriptor: .init(sortBy: [.init(\.date)])).get().filter({ $0.ticker == dto.ticker })
+ public func isValidatedSaveTrade(_ trade: TradeDTO) -> Bool {
+ if trade.side == .buy { return true }
+ guard let trades = try? fetchTrades(descriptor: .init()).get().filter({ $0.ticker == trade.ticker }).sorted(by: { $0.date < $1.date })
else { return false }
let currentVolume = trades.reduce(0) { (result, trade) in
if trade.side == .buy {
- return result + (trade.volume ?? 0)
+ return result + trade.quantity
} else {
- return result - (trade.volume ?? 0)
+ return result - trade.quantity
}
}
- return currentVolume - (dto.volume ?? 0) > 0
+ return currentVolume - trade.quantity >= 0
}
- public func isValidatedUpdateTrade(origin: Trade, new: TradeDTO) -> Bool {
- if origin.side == .sell && new.side == .buy { return true }
-
- guard let trades = try? fetchTrades(descriptor: .init(sortBy: [.init(\.date)])).get().filter({ $0.ticker == origin.ticker })
+ public func isValidatedUpdateTrade(_ trade: Trade, new newTrade: TradeDTO) -> Bool {
+ if trade.side == .sell && newTrade.side == .buy { return true }
+ guard let trades = try? fetchTrades(descriptor: .init()).get().filter({ $0.ticker == trade.ticker }).sorted(by: { $0.date < $1.date })
else { return false }
var currentVolume = 0.0
- for trade in trades {
+ for tmpTrade in trades {
if trade.side == .buy {
- currentVolume += trade == origin ? (new.volume ?? 0) : (trade.volume ?? 0)
+ currentVolume += tmpTrade == trade ? newTrade.quantity : tmpTrade.quantity
} else {
- currentVolume -= trade == origin ? (new.volume ?? 0) : (trade.volume ?? 0)
+ currentVolume -= tmpTrade == trade ? newTrade.quantity : tmpTrade.quantity
}
if currentVolume < 0 {
return false
}
}
-
return true
}
- public func isValidatedDeleteTrade(origin: Trade) -> Bool {
- if origin.side == .sell { return true }
+ public func isValidatedDeleteTrade(_ trade: Trade) -> Bool {
+ if trade.side == .sell { return true }
- guard let trades = try? fetchTrades(descriptor: .init(sortBy: [.init(\.date)])).get().filter({ $0.ticker == origin.ticker })
+ guard let trades = try? fetchTrades(descriptor: .init(sortBy: [.init(\.date)])).get().filter({ $0.ticker == trade.ticker })
else { return false }
var currentVolume = 0.0
- for trade in trades {
+ for tmpTrade in trades {
if trade.side == .buy {
- currentVolume += trade == origin ? 0 : (trade.volume ?? 0)
+ currentVolume += tmpTrade == trade ? 0 : tmpTrade.quantity
} else {
- currentVolume -= trade == origin ? 0 : (trade.volume ?? 0)
+ currentVolume -= tmpTrade == trade ? 0 : tmpTrade.quantity
}
if currentVolume < 0 {
return false
}
}
-
return true
}
}
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/TagClient.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/TagClient.swift
new file mode 100644
index 0000000..d2d7e03
--- /dev/null
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/TagClient.swift
@@ -0,0 +1,92 @@
+//
+// TagClient.swift
+// ToolinderDomainTradeInterface
+//
+// Created by 송영모 on 2023/10/01.
+//
+
+import Foundation
+import SwiftData
+
+import ComposableArchitecture
+
+public enum TagError: Error {
+ case unknown
+}
+
+public struct TagClient {
+ public static let tagRepository: TagRepositoryInterface = TagRepository()
+
+ public var fetchTags: () -> Result<[Tag], TagError>
+ public var saveTag: (TagDTO) -> Result
+ public var updateTag: (Tag, TagDTO) -> Result
+ public var deleteTag: (Tag) -> Result
+
+ public var isValidatedSaveTag: (TagDTO) -> Bool
+ public var isValidatedUpdateTag: (Tag, TagDTO) -> Bool
+ public var isValidatedDeleteTag: (Tag) -> Bool
+
+ public init(
+ fetchTags: @escaping () -> Result<[Tag], TagError>,
+ saveTag: @escaping (TagDTO) -> Result,
+ updateTag: @escaping (Tag, TagDTO) -> Result,
+ deleteTag: @escaping (Tag) -> Result,
+
+ isValidatedSaveTag: @escaping (TagDTO) -> Bool,
+ isValidatedUpdateTag: @escaping (Tag, TagDTO) -> Bool,
+ isValidatedDeleteTag: @escaping (Tag) -> Bool
+ ) {
+ self.fetchTags = fetchTags
+ self.saveTag = saveTag
+ self.updateTag = updateTag
+ self.deleteTag = deleteTag
+
+ self.isValidatedSaveTag = isValidatedSaveTag
+ self.isValidatedUpdateTag = isValidatedUpdateTag
+ self.isValidatedDeleteTag = isValidatedDeleteTag
+ }
+}
+
+extension TagClient: TestDependencyKey {
+ public static var previewValue: TagClient = Self(
+ fetchTags: { return .failure(.unknown) },
+ saveTag: { _ in return .failure(.unknown) },
+ updateTag: { _, _ in return .failure(.unknown) },
+ deleteTag: { _ in return .failure(.unknown) },
+
+ isValidatedSaveTag: { _ in return true },
+ isValidatedUpdateTag: { _, _ in return true },
+ isValidatedDeleteTag: { _ in return true }
+ )
+
+ public static var testValue = Self(
+ fetchTags: unimplemented("\(Self.self).fetchTags"),
+ saveTag: unimplemented("\(Self.self).saveTag"),
+ updateTag: unimplemented("\(Self.self).updateTag"),
+ deleteTag: unimplemented("\(Self.self).deleteTag"),
+
+ isValidatedSaveTag: unimplemented("\(Self.self).isValidatedSaveTag"),
+ isValidatedUpdateTag: unimplemented("\(Self.self).isValidatedUpdateTag"),
+ isValidatedDeleteTag: unimplemented("\(Self.self).isValidatedDeleteTag")
+ )
+}
+
+public extension DependencyValues {
+ var tagClient: TagClient {
+ get { self[TagClient.self] }
+ set { self[TagClient.self] = newValue }
+ }
+}
+
+extension TagClient: DependencyKey {
+ public static var liveValue = TagClient(
+ fetchTags: { tagRepository.fetchTags(descriptor: .init()) },
+ saveTag: { tagRepository.saveTag($0) },
+ updateTag: { tagRepository.updateTag($0, new: $1) },
+ deleteTag: { tagRepository.deleteTag($0) },
+
+ isValidatedSaveTag: { tagRepository.isValidatedSaveTag($0) },
+ isValidatedUpdateTag: { tagRepository.isValidatedUpdateTag($0, new: $1) },
+ isValidatedDeleteTag: { tagRepository.isValidatedDeleteTag($0) }
+ )
+}
diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/TradeClient.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/TradeClient.swift
index deb9074..7defe95 100644
--- a/Projects/Toolinder/Domain/Trade/Interface/Sources/TradeClient.swift
+++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/TradeClient.swift
@@ -61,8 +61,8 @@ public extension DependencyValues {
extension TradeClient: DependencyKey {
public static var liveValue = TradeClient(
fetchTrades: { tradeRepository.fetchTrades(descriptor: .init()) },
- saveTrade: { tradeRepository.saveTrade(dto: $0) },
- updateTrade: { tradeRepository.updateTrade(model:$0, dto: $1) },
- deleteTrade: { tradeRepository.deleteTrade(trade: $0) }
+ saveTrade: { tradeRepository.saveTrade($0) },
+ updateTrade: { tradeRepository.updateTrade($0, new: $1) },
+ deleteTrade: { tradeRepository.deleteTrade($0) }
)
}
diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarStore.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarStore.swift
index 30ebace..ca8c466 100644
--- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarStore.swift
+++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarStore.swift
@@ -43,8 +43,8 @@ public struct CalendarStore: Reducer {
public var calendarItem: IdentifiedArrayOf = []
public var tradeItem: IdentifiedArrayOf = []
- @PresentationState var tickerEdit: TickerEditStore.State?
- @PresentationState var tradeEdit: TradeEditStore.State?
+ @PresentationState var selectTicker: SelectTickerStore.State?
+ @PresentationState var editTrade: EditTradeStore.State?
public init(
id: UUID = .init(),
@@ -83,8 +83,8 @@ public struct CalendarStore: Reducer {
case calendarItem(id: CalendarItemCellStore.State.ID, action: CalendarItemCellStore.Action)
case tradeItem(id: TradeItemCellStore.State.ID, action: TradeItemCellStore.Action)
- case tickerEdit(PresentationAction)
- case tradeEdit(PresentationAction)
+ case selectTicker(PresentationAction)
+ case editTrade(PresentationAction)
case delegate(Delegate)
@@ -104,7 +104,7 @@ public struct CalendarStore: Reducer {
return .none
case .newButtonTapped:
- state.tickerEdit = .init()
+ state.selectTicker = .init()
return .none
case let .tradeItemTapped(trade):
@@ -142,31 +142,27 @@ public struct CalendarStore: Reducer {
return .none
}
- case .tickerEdit(.presented(.delegate(.cancel))):
- state.tickerEdit = nil
+ case let .selectTicker(.presented(.delegate(.select(ticker)))):
+ state.selectTicker = nil
+ state.editTrade = .init(selectedTicker: ticker, selectedDate: state.selectedDate)
return .none
- case let .tickerEdit(.presented(.delegate(.next(ticker)))):
- state.tickerEdit = nil
- state.tradeEdit = .init(selectedTicker: ticker, selectedDate: state.selectedDate)
+ case .selectTicker(.dismiss):
+ state.selectTicker = nil
return .none
- case .tickerEdit(.dismiss):
- state.tickerEdit = nil
+ case .editTrade(.presented(.delegate(.save))):
+ state.selectTicker = nil
+ state.editTrade = nil
return .none
- case .tradeEdit(.presented(.delegate(.save))):
- state.tickerEdit = nil
- state.tradeEdit = nil
+ case let .editTrade(.presented(.delegate(.cancel(ticker)))):
+ state.selectTicker = .init(selectedTicker: ticker)
+ state.editTrade = nil
return .none
- case let .tradeEdit(.presented(.delegate(.cancel(ticker)))):
- state.tickerEdit = .init(selectedTicker: ticker)
- state.tradeEdit = nil
- return .none
-
- case .tradeEdit(.dismiss):
- state.tradeEdit = nil
+ case .editTrade(.dismiss):
+ state.editTrade = nil
return .none
default:
@@ -179,11 +175,11 @@ public struct CalendarStore: Reducer {
.forEach(\.tradeItem, action: /Action.tradeItem(id:action:)) {
TradeItemCellStore()
}
- .ifLet(\.$tickerEdit, action: /Action.tickerEdit) {
- TickerEditStore()
+ .ifLet(\.$selectTicker, action: /Action.selectTicker) {
+ SelectTickerStore()
}
- .ifLet(\.$tradeEdit, action: /Action.tradeEdit) {
- TradeEditStore()
+ .ifLet(\.$editTrade, action: /Action.editTrade) {
+ EditTradeStore()
}
}
}
diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarView.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarView.swift
index 666d93f..8707c54 100644
--- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarView.swift
+++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarView.swift
@@ -48,20 +48,20 @@ public struct CalendarView: View {
}
.sheet(
store: self.store.scope(
- state: \.$tickerEdit,
- action: { .tickerEdit($0) }
+ state: \.$selectTicker,
+ action: { .selectTicker($0) }
)
) {
- TickerEditView(store: $0)
+ SelectTickerView(store: $0)
.presentationDetents([.medium])
}
.sheet(
store: self.store.scope(
- state: \.$tradeEdit,
- action: { .tradeEdit($0) }
+ state: \.$editTrade,
+ action: { .editTrade($0) }
)
) {
- TradeEditView(store: $0)
+ EditTradeView(store: $0)
.presentationDetents([.medium])
}
.tag(viewStore.offset)
diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellView.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellView.swift
index 6652866..24a7aa6 100644
--- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellView.swift
+++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellView.swift
@@ -29,7 +29,7 @@ public struct CalendarItemCellView: View {
Text("\(viewStore.state.date.day)")
.font(.subheadline)
.fontWeight(.semibold)
- .foregroundStyle(Color.blackOrWhite(!viewStore.state.isSelected))
+ .foregroundStyle(viewStore.state.isSelected ? Color.background : Color.foreground)
Spacer()
}
@@ -41,7 +41,7 @@ public struct CalendarItemCellView: View {
Spacer()
}
- .background(Color.blackOrWhite(viewStore.state.isSelected))
+ .background(viewStore.state.isSelected ? Color.foreground : Color.background)
.clipShape(
RoundedRectangle(
cornerRadius: 8,
diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/CalendarMain/CalendarMainStore.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/CalendarMain/CalendarMainStore.swift
index 5c83817..010a35d 100644
--- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/CalendarMain/CalendarMainStore.swift
+++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/CalendarMain/CalendarMainStore.swift
@@ -123,10 +123,10 @@ public struct CalendarMainStore: Reducer {
case .delegate(.refresh):
return .send(.fetch)
- case .tradeEdit(.presented(.delegate(.save))):
+ case .editTrade(.presented(.delegate(.save))):
return .send(.fetch)
- case .tradeEdit(.dismiss):
+ case .editTrade(.dismiss):
return .send(.fetch)
case let .delegate(.detail(trade)):
diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/ExistingUserPolicy/ExistingUserPolicyView.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/ExistingUserPolicy/ExistingUserPolicyView.swift
deleted file mode 100644
index d851b3e..0000000
--- a/Projects/Toolinder/Feature/MyPage/Interface/Sources/ExistingUserPolicy/ExistingUserPolicyView.swift
+++ /dev/null
@@ -1,50 +0,0 @@
-//
-// ExistingUserPolicyView.swift
-// ToolinderFeatureMyPageDemo
-//
-// Created by 송영모 on 2023/09/14.
-//
-
-import SwiftUI
-
-import ComposableArchitecture
-
-public struct ExistingUserPolicyView: View {
- let store: StoreOf
-
- public init(store: StoreOf) {
- self.store = store
- }
-
- public var body: some View {
- WithViewStore(self.store, observe: { $0 }) { viewStore in
- ScrollView {
- HStack {
- VStack(alignment: .leading) {
- Text("1 버전을 사용해주신 고마운 사용자분들께")
- .font(.body)
- .padding(.bottom, 5)
-
- Text("여러분께 아주 죄송한 마음 뿐입니다.")
- .font(.headline)
-
- Text("우선 지금부터 하는 얘기는 기존 사용자들의 데이터를 살리지 못하는 결정을 내리게된 이유를 설명드리려고 합니다. 앱을 업데이트 했는데, 데이터가 모두 사라지는 현상이 있었습니다. 그래서 이를 해결하고자 하였지만, 아주 오래전 작성된 코드(물론 제가 작성했었음)는 문제가 있었고 현재 모두 새로운 것으로 고쳤습니다. 그래서 현재 버전부터는 모든 문제가 해결되었지만, 기존에 발생하고 있던 문제는 심지어 앱을 강제로 꺼지는 버그도 존재했습니다. 이를 해결하는 원인 파악이 어렵고 꽤 많은 곳에서 버그들이 발생하기 때문에 아예 새로 다시 만들자는 판단을 하였습니다.")
- .font(.caption)
- .padding(.bottom, 5)
-
- Text("더 나은 사용성을 위하여 피드백을 적극적으로 수용하겠습니다.")
- .font(.headline)
-
- Text("앞으로는 유저 여러분의 소중한 피드백을 하나하나씩 반영해 나가겠습니다! 긴 글을 읽어주셔서 감사합니다. 다시 한번 죄송합니다. 마이페이지의 설문을 간단하게 달아놓았습니다. 언제든지 피드백을 주시면 곧바로 반영하겠습니다.")
- .font(.caption)
- .padding(.bottom, 5)
- }
-
- Spacer()
- }
- .padding()
- }
- .navigationTitle("Existing User Policy")
- }
- }
-}
diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainStore.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainStore.swift
index 5fe8f74..e25e583 100644
--- a/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainStore.swift
+++ b/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainStore.swift
@@ -20,10 +20,10 @@ public struct MyPageMainStore: Reducer {
case onAppear
case delegate(Delegate)
- case existingUserPolicyTapped
+ case whatIsNew
public enum Delegate: Equatable {
- case existingUserPolicy
+ case whatIsNew
}
}
@@ -33,8 +33,8 @@ public struct MyPageMainStore: Reducer {
case .onAppear:
return .none
- case .existingUserPolicyTapped:
- return .send(.delegate(.existingUserPolicy))
+ case .whatIsNew:
+ return .send(.delegate(.whatIsNew))
default:
return .none
diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainView.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainView.swift
index 102b59c..76726ae 100644
--- a/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainView.swift
+++ b/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainView.swift
@@ -46,7 +46,7 @@ public struct MyPageMainView: View {
HStack {
Label(
title: {
- Text("Existing User Policy")
+ Text("What's New \(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "")")
}, icon: {
Image(systemName: "info.circle.fill")
.foregroundStyle(.blue)
@@ -55,7 +55,7 @@ public struct MyPageMainView: View {
Spacer()
Button(
action: {
- viewStore.send(.existingUserPolicyTapped)
+ viewStore.send(.whatIsNew)
},
label: {
Image(systemName: "chevron.right")
diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackStore.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackStore.swift
index 6260252..c730365 100644
--- a/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackStore.swift
+++ b/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackStore.swift
@@ -29,16 +29,16 @@ public struct MyPageNavigationStackStore: Reducer {
public struct Path: Reducer {
public enum State: Equatable {
- case existingUserPolicy(ExistingUserPolicyStore.State)
+ case whatIsNew(WhatIsNewStore.State)
}
public enum Action: Equatable {
- case existingUserPolicy(ExistingUserPolicyStore.Action)
+ case whatIsNew(WhatIsNewStore.Action)
}
public var body: some Reducer {
- Scope(state: /State.existingUserPolicy, action: /Action.existingUserPolicy) {
- ExistingUserPolicyStore()
+ Scope(state: /State.whatIsNew, action: /Action.whatIsNew) {
+ WhatIsNewStore()
}
}
}
@@ -51,8 +51,8 @@ public struct MyPageNavigationStackStore: Reducer {
case .onAppear:
return .none
- case .main(.delegate(.existingUserPolicy)):
- state.path.append(.existingUserPolicy(.init()))
+ case .main(.delegate(.whatIsNew)):
+ state.path.append(.whatIsNew(.init()))
return .none
default:
diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackView.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackView.swift
index 723eca3..fc48edd 100644
--- a/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackView.swift
+++ b/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackView.swift
@@ -33,11 +33,11 @@ public struct MyPageNavigationStackView: View {
}
} destination: {
switch $0 {
- case .existingUserPolicy:
+ case .whatIsNew:
CaseLet(
- /MyPageNavigationStackStore.Path.State.existingUserPolicy,
- action: MyPageNavigationStackStore.Path.Action.existingUserPolicy,
- then: ExistingUserPolicyView.init(store:))
+ /MyPageNavigationStackStore.Path.State.whatIsNew,
+ action: MyPageNavigationStackStore.Path.Action.whatIsNew,
+ then: WhatIsNewView.init(store:))
}
}
}
diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/ExistingUserPolicy/ExistingUserPolicyStore.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/WhatIsNew/WhatIsNewStore.swift
similarity index 91%
rename from Projects/Toolinder/Feature/MyPage/Interface/Sources/ExistingUserPolicy/ExistingUserPolicyStore.swift
rename to Projects/Toolinder/Feature/MyPage/Interface/Sources/WhatIsNew/WhatIsNewStore.swift
index 164d162..3bef097 100644
--- a/Projects/Toolinder/Feature/MyPage/Interface/Sources/ExistingUserPolicy/ExistingUserPolicyStore.swift
+++ b/Projects/Toolinder/Feature/MyPage/Interface/Sources/WhatIsNew/WhatIsNewStore.swift
@@ -9,7 +9,7 @@ import Foundation
import ComposableArchitecture
-public struct ExistingUserPolicyStore: Reducer {
+public struct WhatIsNewStore: Reducer {
public init() {}
public struct State: Equatable {
diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/WhatIsNew/WhatIsNewView.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/WhatIsNew/WhatIsNewView.swift
new file mode 100644
index 0000000..06ae633
--- /dev/null
+++ b/Projects/Toolinder/Feature/MyPage/Interface/Sources/WhatIsNew/WhatIsNewView.swift
@@ -0,0 +1,51 @@
+//
+// ExistingUserPolicyView.swift
+// ToolinderFeatureMyPageDemo
+//
+// Created by 송영모 on 2023/09/14.
+//
+
+import SwiftUI
+
+import ComposableArchitecture
+
+public struct WhatIsNewView: View {
+ let store: StoreOf
+
+ public init(store: StoreOf) {
+ self.store = store
+ }
+
+ public var body: some View {
+ WithViewStore(self.store, observe: { $0 }) { viewStore in
+ ScrollView {
+ HStack {
+ VStack(alignment: .leading) {
+ Text("투린더를 사용해주신 고마운 사용자분들께")
+ .font(.body)
+ .padding(.bottom, 5)
+
+ Text(
+"""
+유저가 애초에 많은 서비스는 아니었지만, 꾸준히 사용해주시는 10명 정도의 사용자분들께 작성하는 편지라고 생각하시면 감사할 것 같습니다.
+우선 먼저 감사하다는 말씀을 드립니다. 댓글도 별로 안달리고 평점도 낮은 앱이지만 작은 관심은 항상 큰 도움이 되었습니다. 그리고 업데이트를 결정한 이유도 모두 꾸준히 사용해주시는 분들이 계셔서라고 할 수 있습니다.
+
+하지만 이 다음부터 하는 이야기는 모두 죄송하다는 말 뿐이어서 저도 속상한 마음으로 작성 중입니다. 기술적으로 투린더의 데이터를 살리지 못하는 버그가 존재했고, 모든 것을 살리기에는 시간적으로 많은 시간이 걸리는 것으로 파악을 하였습니다. 저의 첫번째 앱이기도 하고 그때 당시에 실력이 좋지 못해서 큰 그림을 그리며 설계를 하지 못해서 이렇게 되었습니다. 살리려고 노력을 많이 해봤지만, 아예 구조자체를 바꾸려고 하는 상황이어서 너무 많은 시간과 노력이 필요하다는 결론을 지었습니다. 그곳에 사용하는 시간을 앞으로의 새로운 앱에 집중하고자 하였습니다.
+
+이런 상황은 다시 발생해서는 안된다고 생각합니다. 앞으로는 더욱더 나은 서비스를 만들도록 노력하겠습니다.
+
+긴 글 읽어주셔서 감사합니다.
+"""
+ )
+ .font(.caption)
+ .padding(.bottom, 5)
+ }
+
+ Spacer()
+ }
+ .padding()
+ }
+ .navigationTitle("What's New \(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "")")
+ }
+ }
+}
diff --git a/Projects/Toolinder/Feature/Portfolio/Interface/Sources/Main/PortfolioMainView.swift b/Projects/Toolinder/Feature/Portfolio/Interface/Sources/Main/PortfolioMainView.swift
index fb5afa0..6c0631a 100644
--- a/Projects/Toolinder/Feature/Portfolio/Interface/Sources/Main/PortfolioMainView.swift
+++ b/Projects/Toolinder/Feature/Portfolio/Interface/Sources/Main/PortfolioMainView.swift
@@ -11,6 +11,7 @@ import Charts
import ComposableArchitecture
import ToolinderFeatureTradeInterface
+import ToolinderShared
public struct PortfolioMainView: View {
public let store: StoreOf
diff --git a/Projects/Toolinder/Feature/Sources/MainTabView.swift b/Projects/Toolinder/Feature/Sources/MainTabView.swift
index 23e4705..a0fb1bb 100644
--- a/Projects/Toolinder/Feature/Sources/MainTabView.swift
+++ b/Projects/Toolinder/Feature/Sources/MainTabView.swift
@@ -47,7 +47,7 @@ public struct MainTabView: View {
.onAppear {
viewStore.send(.onAppear)
}
- .accentColor(Color.blackOrWhite(true))
+ .accentColor(Color.foreground)
}
}
}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TagItemCellStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TagItemCellStore.swift
new file mode 100644
index 0000000..f232bd7
--- /dev/null
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TagItemCellStore.swift
@@ -0,0 +1,68 @@
+//
+// TagItemCellStore.swift
+// ToolinderFeatureTradeInterface
+//
+// Created by 송영모 on 2023/10/01.
+//
+
+import Foundation
+
+import ComposableArchitecture
+
+import ToolinderDomain
+
+public struct TagItemCellStore: Reducer {
+ public init() {}
+
+ public struct State: Equatable, Identifiable {
+ public let mode: EditMode
+ public let id: UUID
+
+ public let tag: Tag
+ public var isSelected: Bool
+
+ public init(
+ mode: EditMode = .edit,
+ id: UUID = .init(),
+ tag: Tag,
+ isSelected: Bool = false
+ ) {
+ self.mode = mode
+ self.id = id
+ self.tag = tag
+ self.isSelected = isSelected
+ }
+ }
+
+ public enum Action: Equatable {
+ case onAppear
+
+ case tapped
+ case editButtonTapped
+
+ case delegate(Delegate)
+
+ public enum Delegate: Equatable {
+ case tapped
+ case editButtonTapped
+ }
+ }
+
+ public var body: some ReducerOf {
+ Reduce { state, action in
+ switch action {
+ case .onAppear:
+ return .none
+
+ case .tapped:
+ return .send(.delegate(.tapped))
+
+ case .editButtonTapped:
+ return .send(.delegate(.editButtonTapped))
+
+ default:
+ return .none
+ }
+ }
+ }
+}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TagItemCellView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TagItemCellView.swift
new file mode 100644
index 0000000..e9a9206
--- /dev/null
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TagItemCellView.swift
@@ -0,0 +1,55 @@
+//
+// TagItemCellView.swift
+// ToolinderFeatureTradeInterface
+//
+// Created by 송영모 on 2023/10/01.
+//
+
+import SwiftUI
+
+import ComposableArchitecture
+
+import ToolinderDomain
+import ToolinderShared
+
+public struct TagItemCellView: View {
+ private let store: StoreOf
+
+ public init(store: StoreOf) {
+ self.store = store
+ }
+
+ public var body: some View {
+ WithViewStore(self.store, observe: { $0 }) { viewStore in
+ HStack(spacing: 10) {
+ Circle()
+ .fill(Color(hex: viewStore.state.tag.hex))
+ .frame(width: 15, height: 15)
+
+ Text(viewStore.state.tag.name)
+ .font(.caption2)
+ .foregroundStyle(.foreground)
+
+ if viewStore.state.mode == .edit {
+ Button(action: {
+ viewStore.send(.editButtonTapped)
+ }, label: {
+ Image(systemName: "pencil.circle.fill")
+ })
+ .foregroundStyle(.foreground)
+ }
+ }
+ .padding(10)
+ .background(viewStore.state.isSelected ? Color(uiColor: .systemGray5) : Color(uiColor: .systemGray6))
+ .clipShape(
+ RoundedRectangle(
+ cornerRadius: 8,
+ style: .continuous
+ )
+ )
+ .onTapGesture {
+ viewStore.send(.tapped)
+ }
+ }
+ }
+}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TickerItemCellView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TickerItemCellView.swift
index c800732..4ed1f95 100644
--- a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TickerItemCellView.swift
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TickerItemCellView.swift
@@ -21,68 +21,83 @@ public struct TickerItemCellView: View {
public var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
- HStack {
- tradeView(viewStore: viewStore)
- }
- }
- }
-
- private func tradeView(viewStore: ViewStoreOf) -> some View {
- HStack(spacing: 10) {
- VStack {
- viewStore.state.ticker.type.image
- .font(.title3)
+ HStack(spacing: 10) {
+ symbolView(viewStore: viewStore)
+
+ nameView(viewStore: viewStore)
if viewStore.mode == .item {
- Text("\(viewStore.state.ticker.type.rawValue)")
- .font(.caption2)
- .frame(width: 40)
+ Spacer()
+
+ tradeSummaryView(viewStore: viewStore)
}
}
+ .onTapGesture {
+ viewStore.send(.tapped)
+ }
+ .frame(height: 35)
+ .padding(10)
+ .background(viewStore.state.isSelected ? Color(uiColor: .systemGray5) : Color(uiColor: .systemGray6))
+ .clipShape(
+ RoundedRectangle(
+ cornerRadius: 8
+ )
+ )
+ }
+ }
+
+ private func symbolView(viewStore: ViewStoreOf) -> some View {
+ VStack {
+ viewStore.state.ticker.type.image
+ .font(.title3)
+ if viewStore.mode == .item {
+ Text("\(viewStore.state.ticker.type.rawValue)")
+ .font(.caption2)
+ .frame(width: 40)
+ }
+ }
+ }
+
+ private func nameView(viewStore: ViewStoreOf) -> some View {
+ HStack {
Text("\(viewStore.state.ticker.name) \(viewStore.state.ticker.trades?.count ?? 0)" )
.font(.body)
.fontWeight(.semibold)
- if viewStore.mode == .item {
+ HStack(spacing: 1) {
+ ForEach(viewStore.state.ticker.tags ?? [], id: \.self) { tag in
+ Circle()
+ .fill(Color(hex: tag.hex))
+ .frame(width: 5, height: 5)
+ }
+ }
+ }
+ }
+
+ private func tradeSummaryView(viewStore: ViewStoreOf) -> some View {
+ VStack {
+ HStack {
+ Spacer()
+ Text("\(Int(viewStore.tickerSummaryDataEntity.profit)) \(viewStore.state.ticker.currency.rawValue) (\(Int(viewStore.tickerSummaryDataEntity.yield))%)")
+ .font(.caption)
+ .fontWeight(.semibold)
+ .foregroundStyle(Int(viewStore.tickerSummaryDataEntity.yield) > 0 ? .pink : .mint)
+ }
+
+ HStack(spacing: .zero) {
Spacer()
- VStack {
- HStack {
- Spacer()
-
- Text("\(Int(viewStore.tickerSummaryDataEntity.profit)) \(viewStore.state.ticker.currency.rawValue) (\(Int(viewStore.tickerSummaryDataEntity.yield))%)")
- .font(.caption)
- .fontWeight(.semibold)
- .foregroundStyle(Int(viewStore.tickerSummaryDataEntity.yield) > 0 ? .pink : .mint)
- }
-
- HStack(spacing: .zero) {
- Spacer()
-
- Text("\(Int(viewStore.tickerSummaryDataEntity.avgPrice)) \(viewStore.state.ticker.currency.rawValue)")
- .font(.caption2)
- }
-
- HStack(spacing: .zero) {
- Spacer()
+ Text("\(Int(viewStore.tickerSummaryDataEntity.avgPrice)) \(viewStore.state.ticker.currency.rawValue)")
+ .font(.caption2)
+ }
+
+ HStack(spacing: .zero) {
+ Spacer()
- Text("\(Int(viewStore.tickerSummaryDataEntity.currentVolume)) vol")
- .font(.caption2)
- }
- }
+ Text("\(Int(viewStore.tickerSummaryDataEntity.currentVolume)) vol")
+ .font(.caption2)
}
}
- .frame(height: 35)
- .padding(10)
- .background(viewStore.state.isSelected ? Color(uiColor: .systemGray5) : Color(uiColor: .systemGray6))
- .clipShape(
- RoundedRectangle(
- cornerRadius: 8
- )
- )
- .onTapGesture {
- viewStore.send(.tapped)
- }
}
}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradeItemCellView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradeItemCellView.swift
index f20f91f..0e311b1 100644
--- a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradeItemCellView.swift
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradeItemCellView.swift
@@ -58,7 +58,6 @@ public struct TradeItemCellView: View {
.font(.caption2)
}
}
- .frame(width: 70)
viewStore.state.trade.ticker?.type.image
.font(.title3)
@@ -74,7 +73,7 @@ public struct TradeItemCellView: View {
Text("\(Int(viewStore.state.trade.price)) \(viewStore.state.trade.ticker?.currency.rawValue ?? "")")
.font(.caption)
- Text("\(Int(viewStore.state.trade.volume)) vol")
+ Text("\(Int(viewStore.state.trade.quantity)) vol")
.font(.caption)
}
}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradePreviewItemCellView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradePreviewItemCellView.swift
index 1ed4e76..991ee2e 100644
--- a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradePreviewItemCellView.swift
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradePreviewItemCellView.swift
@@ -29,7 +29,7 @@ public struct TradePreviewItemCellView: View {
Text(viewStore.state.trade.ticker?.name ?? "")
.font(.caption2)
.fontWeight(.light)
- .foregroundStyle(Color.blackOrWhite(!viewStore.state.isSelected))
+ .foregroundStyle(viewStore.state.isSelected ? Color.background : Color.foreground)
Spacer()
}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/View/EditHeaderView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/View/EditHeaderView.swift
new file mode 100644
index 0000000..9e9d4c2
--- /dev/null
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/View/EditHeaderView.swift
@@ -0,0 +1,87 @@
+//
+// EditHeaderView.swift
+// ToolinderFeatureTradeInterface
+//
+// Created by 송영모 on 2023/10/02.
+//
+
+import Foundation
+import SwiftUI
+
+public enum EditMode {
+ case add
+ case edit
+ case select
+
+ public enum Action {
+ case dismiss
+ case new
+ case delete
+ }
+}
+
+public struct EditHeaderView: View {
+ public let mode: EditMode
+ public let title: LocalizedStringKey
+ public let isShowDismissButton: Bool
+ public let isShowNewButton: Bool
+ public let isShowDeleteButton: Bool
+
+ public var action: (EditMode.Action) -> ()
+
+ public init(
+ mode: EditMode,
+ title: LocalizedStringKey,
+ isShowDismissButton: Bool = false,
+ isShowNewButton: Bool = false,
+ isShowDeleteButton: Bool = false,
+ action: @escaping (EditMode.Action) -> Void
+ ) {
+ self.mode = mode
+ self.title = title
+ self.isShowDismissButton = isShowDismissButton
+ self.isShowNewButton = isShowNewButton
+ self.isShowDeleteButton = isShowDeleteButton
+
+ self.action = action
+ }
+
+ public var body: some View {
+ HStack {
+ if isShowDismissButton {
+ Button(action: {
+ action(.dismiss)
+ }, label: {
+ Image(systemName: "chevron.left")
+ .font(.title)
+ .foregroundStyle(.foreground)
+ })
+ }
+
+ Text(title)
+ .font(.title)
+
+ Spacer()
+
+ if isShowDeleteButton {
+ Button(action: {
+ action(.delete)
+ }, label: {
+ Image(systemName: "trash.circle.fill")
+ .foregroundStyle(.foreground)
+ .font(.title)
+ })
+ }
+
+ if isShowNewButton {
+ Button(action: {
+ action(.new)
+ }, label: {
+ Image(systemName: "plus.circle.fill")
+ .foregroundStyle(.foreground)
+ .font(.title)
+ })
+ }
+ }
+ }
+}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTag/EditTagStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTag/EditTagStore.swift
new file mode 100644
index 0000000..24c9b5a
--- /dev/null
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTag/EditTagStore.swift
@@ -0,0 +1,96 @@
+//
+// EditTagStore.swift
+// ToolinderFeatureTradeInterface
+//
+// Created by 송영모 on 2023/10/02.
+//
+
+import Foundation
+import SwiftUI
+
+import ComposableArchitecture
+
+import ToolinderDomain
+
+public struct EditTagStore: Reducer {
+ public init() {}
+
+ public struct State: Equatable {
+ public var mode: EditMode
+ public var tag: Tag?
+
+ public var title: LocalizedStringKey = ""
+
+ public var tagName: String = ""
+ public var tagColor: Color = .foreground
+
+ public init(
+ mode: EditMode,
+ tag: Tag? = nil
+ ) {
+ self.mode = mode
+ self.tag = tag
+
+ if mode == .edit {
+ self.tagName = tag?.name ?? ""
+ self.tagColor = Color(hex: tag?.hex ?? "")
+ }
+ }
+ }
+
+ public enum Action: Equatable {
+ case onAppear
+
+ case setTagName(String)
+ case setTagColor(Color)
+ case dismissButtonTapped
+ case deleteButtonTapped
+ case saveButtonTapped
+
+ case delegate(Delegate)
+
+ public enum Delegate: Equatable {
+ case cancle
+ case save(Tag)
+ case delete(Tag)
+ }
+ }
+
+ @Dependency(\.tagClient) var tagClient
+
+ public var body: some ReducerOf {
+ Reduce { state, action in
+ switch action {
+ case .onAppear:
+ return .none
+
+ case let .setTagName(name):
+ state.tagName = name
+ return .none
+
+ case let .setTagColor(color):
+ state.tagColor = color
+ return .none
+
+ case .dismissButtonTapped:
+ return .send(.delegate(.cancle))
+
+ case .deleteButtonTapped:
+ if let tag = state.tag, let deletedTag = try? tagClient.deleteTag(tag).get() {
+ return .send(.delegate(.delete(deletedTag)))
+ }
+ return .none
+
+ case .saveButtonTapped:
+ guard state.tagName != "" else { return .none }
+ if let tag = try? tagClient.saveTag(.init(hex: state.tagColor.toHex(), name: state.tagName)).get() {
+ return .send(.delegate(.save(tag)))
+ }
+ return .none
+
+ default:
+ return .none
+ }
+ }
+ }
+}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTag/EditTagView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTag/EditTagView.swift
new file mode 100644
index 0000000..69093e2
--- /dev/null
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTag/EditTagView.swift
@@ -0,0 +1,91 @@
+//
+// EditTagView.swift
+// ToolinderFeatureTradeInterface
+//
+// Created by 송영모 on 2023/10/02.
+//
+
+import Foundation
+import SwiftUI
+
+import ComposableArchitecture
+
+import ToolinderDomain
+import ToolinderShared
+
+public struct EditTagView: View {
+ let store: StoreOf
+
+ public init(store: StoreOf) {
+ self.store = store
+ }
+
+ public var body: some View {
+ WithViewStore(self.store, observe: { $0 }) { viewStore in
+ VStack(alignment: .leading, spacing: 20) {
+ headerView(viewStore: viewStore)
+
+ nameView(viewStore: viewStore)
+
+ colorView(viewStore: viewStore)
+
+ Spacer()
+
+ MinimalButton(title: "Save") {
+ viewStore.send(.saveButtonTapped)
+ }
+ }
+ .padding()
+ }
+ }
+
+ @ViewBuilder
+ private func headerView(viewStore: ViewStoreOf) -> some View {
+ switch viewStore.state.mode {
+ case .add:
+ EditHeaderView(
+ mode: viewStore.state.mode,
+ title: "Tag",
+ isShowDismissButton: true
+ ) { action in
+ switch action {
+ case .dismiss:
+ viewStore.send(.dismissButtonTapped)
+ default: break
+ }
+ }
+ case .edit:
+ EditHeaderView(
+ mode: viewStore.state.mode,
+ title: "Tag",
+ isShowDeleteButton: true
+ ) { action in
+ switch action {
+ case .dismiss:
+ viewStore.send(.dismissButtonTapped)
+ case .delete:
+ viewStore.send(.deleteButtonTapped)
+ default: break
+ }
+ }
+
+ default: EmptyView()
+ }
+ }
+
+ private func nameView(viewStore: ViewStoreOf) -> some View {
+ TextField(
+ text: viewStore.binding(get: \.tagName, send: EditTagStore.Action.setTagName),
+ label: {
+ Label("Name", systemImage: "highlighter")
+ }
+ )
+ .foregroundStyle(.foreground)
+ }
+
+ private func colorView(viewStore: ViewStoreOf) -> some View {
+ ColorPicker(selection: viewStore.binding(get: \.tagColor, send: EditTagStore.Action.setTagColor), label: {
+ Label("Color", systemImage: "paintpalette.fill")
+ })
+ }
+}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerEditStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/EditTickerStore.swift
similarity index 54%
rename from Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerEditStore.swift
rename to Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/EditTickerStore.swift
index d1cef26..47519bd 100644
--- a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerEditStore.swift
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/EditTickerStore.swift
@@ -11,38 +11,49 @@ import ComposableArchitecture
import ToolinderDomainTradeInterface
-public struct TickerEditStore: Reducer {
+public struct EditTickerStore: Reducer {
public init() {}
- public enum Mode {
- case add
- case edit
- }
-
public struct State: Equatable {
- public var mode: Mode
- public var name: String = ""
- public var tickerType: TickerType?
- public var currency: Currency?
+ public var mode: EditMode
+ public var ticker: Ticker?
- public var selectedTicker: Ticker?
+ public var name: String = ""
+ public var selectedTickerType: TickerType?
+ public var selectedCurrency: Currency?
+ public var selectedTags: [Tag] = [] {
+ didSet {
+ tagItem = .init(
+ uniqueElements: selectedTags.map { tag in
+ return .init(tag: tag)
+ }
+ )
+ }
+ }
- public var tickerItem: IdentifiedArrayOf = []
+ public var tagItem: IdentifiedArrayOf = []
@PresentationState var selectTickerType: SelectTickerTypeStore.State?
@PresentationState var selectCurrency: SelectCurrencyStore.State?
+ @PresentationState var selectTag: SelectTagStore.State?
@PresentationState var alert: AlertState?
public init(
- mode: Mode = .add,
- selectedTicker: Ticker? = nil
+ mode: EditMode = .add,
+ ticker: Ticker? = nil
) {
self.mode = mode
- self.selectedTicker = selectedTicker
+ self.ticker = ticker
if mode == .edit {
- self.name = selectedTicker?.name ?? ""
- self.tickerType = selectedTicker?.type ?? .stock
- self.currency = selectedTicker?.currency ?? .dollar
+ self.name = ticker?.name ?? ""
+ self.selectedTickerType = ticker?.type ?? .stock
+ self.selectedCurrency = ticker?.currency ?? .dollar
+ self.selectedTags = ticker?.tags ?? []
+ self.tagItem = .init(
+ uniqueElements: ticker?.tags?.map { tag in
+ return .init(mode: .select, tag: tag)
+ } ?? []
+ )
}
}
}
@@ -51,18 +62,17 @@ public struct TickerEditStore: Reducer {
case onAppear
case setName(String)
- case tickerTapped(Ticker)
- case tickerTypeViewTapped
- case currencyViewTapped
+ case tickerTypeButtonTapped
+ case currencyButtonTapped
+ case tagButtonTapped
+ case dismissButtonTapped
case deleteButtonTapped
- case nextButtonTapped
+ case saveButtonTapped
- case fetchTickersRequest
- case fetchTickersResponse([Ticker])
-
- case tickerItem(id: TickerItemCellStore.State.ID, action: TickerItemCellStore.Action)
+ case tagItem(id: TagItemCellStore.State.ID, action: TagItemCellStore.Action)
case selectTickerType(PresentationAction)
case selectCurrency(PresentationAction)
+ case selectTag(PresentationAction)
case alert(PresentationAction)
case delegate(Delegate)
@@ -72,10 +82,9 @@ public struct TickerEditStore: Reducer {
}
public enum Delegate: Equatable {
- case cancel
- case next(Ticker)
+ case cancle
case save(Ticker)
- case delete(Ticker)
+ case delete
}
}
@@ -85,34 +94,30 @@ public struct TickerEditStore: Reducer {
Reduce { state, action in
switch action {
case .onAppear:
- return .concatenate([
- .send(.fetchTickersRequest)
- ])
+ return .none
case let .setName(name):
state.name = name
return .none
- case let .tickerTapped(ticker):
- if ticker == state.selectedTicker {
- state.selectedTicker = nil
- } else {
- state.selectedTicker = ticker
- }
-
- return .none
-
- case .tickerTypeViewTapped:
+ case .tickerTypeButtonTapped:
state.selectTickerType = .init()
return .none
- case .currencyViewTapped:
+ case .currencyButtonTapped:
state.selectCurrency = .init()
return .none
+ case .tagButtonTapped:
+ state.selectTag = .init(selectedTags: state.selectedTags)
+ return .none
+
+ case .dismissButtonTapped:
+ return .send(.delegate(.cancle))
+
case .deleteButtonTapped:
state.alert = AlertState {
- TextState("\(state.selectedTicker?.trades?.count ?? 0) records are also deleted.")
+ TextState("\(state.ticker?.trades?.count ?? 0) records are also deleted.")
} actions: {
ButtonState(role: .destructive, action: .confirmDeletion) {
TextState("Delete")
@@ -120,66 +125,53 @@ public struct TickerEditStore: Reducer {
}
return .none
- case .nextButtonTapped:
- if let ticker = state.selectedTicker, state.mode == .add {
- return .send(.delegate(.next(ticker)))
- }
-
+ case .saveButtonTapped:
return validateAndSaveTickerEffect(
mode: state.mode,
- ticker: state.selectedTicker,
- tickerType: state.tickerType,
- currency: state.currency,
- name: state.name
+ ticker: state.ticker,
+ tickerType: state.selectedTickerType,
+ currency: state.selectedCurrency,
+ name: state.name,
+ tags: state.selectedTags
)
- case .fetchTickersRequest:
- let tickers = (try? tickerClient.fetchTickers().get()) ?? []
- return .send(.fetchTickersResponse(tickers))
-
- case let .fetchTickersResponse(tickers):
- state.tickerItem = .init(uniqueElements: tickers.map { .init(mode: .preview, ticker: $0) })
- return .none
-
- case let .tickerItem(id: id, action: .delegate(.tapped)):
- let isSelected = state.tickerItem[id: id]?.isSelected ?? false
-
- for id in state.tickerItem.ids {
- state.tickerItem[id: id]?.isSelected = false
- }
-
- state.tickerItem[id: id]?.isSelected = !isSelected
-
- if !isSelected {
- state.selectedTicker = state.tickerItem[id: id]?.ticker
- } else {
- state.selectedTicker = nil
- }
-
- return .none
-
case let .selectTickerType(.presented(.delegate(.select(tickerType)))):
state.selectTickerType = nil
- state.tickerType = tickerType
+ state.selectedTickerType = tickerType
+ return .none
+
+ case let .selectCurrency(.presented(.delegate(.select(currency)))):
+ state.selectCurrency = nil
+ state.selectedCurrency = currency
return .none
case .selectTickerType(.dismiss):
state.selectTickerType = nil
return .none
- case let .selectCurrency(.presented(.delegate(.select(currency)))):
+ case .selectCurrency(.dismiss):
state.selectCurrency = nil
- state.currency = currency
return .none
- case .selectCurrency(.dismiss):
- state.selectCurrency = nil
+ case .selectTag(.dismiss):
+ state.selectTag = nil
+ return .none
+
+ case let .selectTag(.presented(.delegate(.select(tags)))):
+ state.selectTag = nil
+ state.selectedTags = tags
+ return .none
+
+ case let .selectTag(.presented(.delegate(.deleted(tag)))):
+ if let index = state.selectedTags.firstIndex(of: tag) {
+ state.selectedTags.remove(at: index)
+ }
return .none
case .alert(.presented(.confirmDeletion)):
- if let ticker = state.selectedTicker {
+ if let ticker = state.ticker {
let _ = tickerClient.deleteTicker(ticker)
- return .send(.delegate(.delete(ticker)))
+ return .send(.delegate(.delete))
}
return .none
@@ -187,38 +179,41 @@ public struct TickerEditStore: Reducer {
return .none
}
}
- .forEach(\.tickerItem, action: /Action.tickerItem(id:action:)) {
- TickerItemCellStore()
+ .ifLet(\.$selectTag, action: /Action.selectTag) {
+ SelectTagStore()
}
+
.ifLet(\.$alert, action: /Action.alert)
}
private func validateAndSaveTickerEffect(
- mode: Mode,
+ mode: EditMode,
ticker: Ticker?,
tickerType: TickerType?,
currency: Currency?,
- name: String
- ) -> Effect {
+ name: String,
+ tags: [Tag]
+ ) -> Effect {
guard let tickerType = tickerType else { return .none }
guard let currency = currency else { return .none }
guard name.isEmpty == false else { return .none }
switch mode {
case .add:
- if let ticker = try? tickerClient.saveTicker(.init(type: tickerType, currency: currency, name: name)).get() {
- return .send(.delegate(.next(ticker)))
+ if let ticker = try? tickerClient.saveTicker(.init(type: tickerType, currency: currency, name: name, tags: tags)).get() {
+ return .send(.delegate(.save(ticker)))
} else {
return .none
}
case .edit:
guard let ticker = ticker else { return .none }
- if let ticker = try? tickerClient.updateTicker(ticker, .init(type: tickerType, currency: currency, name: name)).get() {
+ if let ticker = try? tickerClient.updateTicker(ticker, .init(type: tickerType, currency: currency, name: name, tags: tags)).get() {
return .send(.delegate(.save(ticker)))
} else {
return .none
}
+ default: return .none
}
}
}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/EditTickerView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/EditTickerView.swift
new file mode 100644
index 0000000..892ec38
--- /dev/null
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/EditTickerView.swift
@@ -0,0 +1,158 @@
+//
+// AddTickerView.swift
+// ToolinderFeatureCalendarDemo
+//
+// Created by 송영모 on 2023/09/07.
+//
+
+import SwiftUI
+import SwiftData
+
+import ComposableArchitecture
+
+import ToolinderDomainTradeInterface
+import ToolinderShared
+
+public struct EditTickerView: View {
+ let store: StoreOf
+
+ public init(store: StoreOf) {
+ self.store = store
+ }
+
+ public var body: some View {
+ WithViewStore(self.store, observe: { $0 }) { viewStore in
+ VStack(alignment: .leading, spacing: 20) {
+ headerView(viewStore: viewStore)
+
+ nameView(viewStore: viewStore)
+
+ tickerTypeView(viewStore: viewStore)
+
+ currencyView(viewStore: viewStore)
+
+ tagView(viewStore: viewStore)
+
+ Spacer()
+
+ MinimalButton(title: "Save") {
+ viewStore.send(.saveButtonTapped)
+ }
+ }
+ .onAppear {
+ viewStore.send(.onAppear)
+ }
+ .sheet(
+ store: self.store.scope(
+ state: \.$selectCurrency,
+ action: { .selectCurrency($0) }
+ )
+ ) {
+ SelectCurrencyView(store: $0)
+ .presentationDetents([.medium])
+ }
+ .sheet(
+ store: self.store.scope(
+ state: \.$selectTickerType,
+ action: { .selectTickerType($0) }
+ )
+ ) {
+ SelectTickerTypeView(store: $0)
+ .presentationDetents([.medium])
+ }
+ .sheet(
+ store: self.store.scope(
+ state: \.$selectTag,
+ action: { .selectTag($0) }
+ )
+ ) {
+ SelectTagView(store: $0)
+ .presentationDetents([.medium])
+ }
+ .alert(
+ store: self.store.scope(
+ state: \.$alert,
+ action: { .alert($0) }
+ )
+ )
+ .padding()
+ }
+ }
+
+ @ViewBuilder
+ private func headerView(viewStore: ViewStoreOf) -> some View {
+ switch viewStore.state.mode {
+ case .add:
+ EditHeaderView(
+ mode: viewStore.state.mode,
+ title: LocalizedStringKey(viewStore.state.ticker?.name ?? "Ticker"),
+ isShowDismissButton: true,
+ isShowDeleteButton: false
+ ) { mode in
+ switch mode {
+ case .dismiss:
+ viewStore.send(.dismissButtonTapped)
+ default: break
+ }
+ }
+
+ case .edit:
+ EditHeaderView(
+ mode: viewStore.state.mode,
+ title: LocalizedStringKey(viewStore.state.ticker?.name ?? "Ticker"),
+ isShowDismissButton: true,
+ isShowDeleteButton: true
+ ) { mode in
+ switch mode {
+ case .dismiss:
+ viewStore.send(.dismissButtonTapped)
+ case .delete:
+ viewStore.send(.deleteButtonTapped)
+ default: break
+ }
+ }
+ default: EmptyView()
+ }
+ }
+
+ private func nameView(viewStore: ViewStoreOf) -> some View {
+ TextField("Name", text: viewStore.binding(get: \.name.localizedUppercase, send: EditTickerStore.Action.setName))
+ .foregroundStyle(.foreground)
+ }
+
+ private func tickerTypeView(viewStore: ViewStoreOf) -> some View {
+ Button(action: {
+ viewStore.send(.tickerTypeButtonTapped)
+ }, label: {
+ Label(viewStore.selectedTickerType?.rawValue ?? "Ticker Type", systemImage: viewStore.selectedTickerType?.systemImageName ?? "questionmark.circle.fill")
+ .foregroundStyle(.foreground)
+ })
+ }
+
+ private func currencyView(viewStore: ViewStoreOf) -> some View {
+ Button(action: {
+ viewStore.send(.currencyButtonTapped)
+ }, label: {
+ Label(viewStore.selectedCurrency?.rawValue ?? "Currency", systemImage: viewStore.selectedCurrency?.systemImageName ?? "questionmark.circle.fill")
+ .foregroundStyle(.foreground)
+ })
+ }
+
+ private func tagView(viewStore: ViewStoreOf) -> some View {
+ ScrollView(.horizontal) {
+ HStack(spacing: .zero) {
+ Button(action: {
+ viewStore.send(.tagButtonTapped)
+ }, label: {
+ Label(viewStore.state.selectedTags.isEmpty ? "Tag" : "", systemImage: "tag.circle.fill")
+ .foregroundStyle(.foreground)
+ })
+
+ ForEachStore(self.store.scope(state: \.tagItem, action: EditTickerStore.Action.tagItem(id:action:))) {
+ TagItemCellView(store: $0)
+ .padding(.trailing)
+ }
+ }
+ }
+ }
+}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerEditView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerEditView.swift
deleted file mode 100644
index 161c5dd..0000000
--- a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerEditView.swift
+++ /dev/null
@@ -1,129 +0,0 @@
-//
-// AddTickerView.swift
-// ToolinderFeatureCalendarDemo
-//
-// Created by 송영모 on 2023/09/07.
-//
-
-import SwiftUI
-import SwiftData
-
-import ComposableArchitecture
-
-import ToolinderDomainTradeInterface
-import ToolinderShared
-
-public struct TickerEditView: View {
- let store: StoreOf
-
- public init(store: StoreOf) {
- self.store = store
- }
-
- public var body: some View {
- WithViewStore(self.store, observe: { $0 }) { viewStore in
- VStack(alignment: .leading, spacing: 20) {
- headerView(viewStore: viewStore)
- .padding([.top, .horizontal])
-
- if viewStore.state.mode == .add {
- tickersView(viewStore: viewStore)
- }
-
- Divider()
- .padding(.horizontal)
-
- inputView(viewStore: viewStore)
- .padding(.horizontal)
-
- Spacer()
-
- MinimalButton(title: "Next") {
- viewStore.send(.nextButtonTapped)
- }
- .padding(.horizontal)
- }
- .onAppear {
- viewStore.send(.onAppear)
- }
- .sheet(
- store: self.store.scope(
- state: \.$selectCurrency,
- action: { .selectCurrency($0) }
- )
- ) { store in
- SelectCurrencyView(store: store)
- .presentationDetents([.medium])
- }
- .sheet(
- store: self.store.scope(
- state: \.$selectTickerType,
- action: { .selectTickerType($0) }
- )
- ) { store in
- SelectTickerTypeView(store: store)
- .presentationDetents([.medium])
- }
- .alert(
- store: self.store.scope(
- state: \.$alert,
- action: { .alert($0) }
- )
- )
- }
- }
-
- private func headerView(viewStore: ViewStoreOf) -> some View {
- HStack {
- if viewStore.state.mode == .add {
- Text("Ticker")
- .font(.title)
- } else {
- Text(viewStore.state.selectedTicker?.name ?? "")
- .font(.title)
- }
-
- Spacer()
-
- if viewStore.state.mode == .edit {
- Button(action: {
- viewStore.send(.deleteButtonTapped)
- }, label: {
- Image(systemName: "trash.circle.fill")
- .foregroundStyle(.foreground)
- .font(.title)
- })
- }
- }
- }
-
- private func tickersView(viewStore: ViewStoreOf) -> some View {
- ScrollView(.horizontal) {
- HStack {
- ForEachStore(self.store.scope(state: \.tickerItem, action: TickerEditStore.Action.tickerItem(id:action:))) {
- TickerItemCellView(store: $0)
- }
- }
- .padding(.horizontal)
- }
- }
-
- private func inputView(viewStore: ViewStoreOf) -> some View {
- VStack(alignment: .leading, spacing: 20) {
- TextField("Name", text: viewStore.binding(get: \.name, send: TickerEditStore.Action.setName))
-
- Button(action: {
- viewStore.send(.tickerTypeViewTapped)
- }, label: {
- Label(viewStore.tickerType?.rawValue ?? "Ticker Type", systemImage: viewStore.tickerType?.systemImageName ?? "questionmark.circle.fill")
- })
-
- Button(action: {
- viewStore.send(.currencyViewTapped)
- }, label: {
- Label(viewStore.currency?.rawValue ?? "Currency", systemImage: viewStore.currency?.systemImageName ?? "questionmark.circle.fill")
- })
- }
- .foregroundStyle(.foreground)
- }
-}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerItem.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerItem.swift
deleted file mode 100644
index 6e214b8..0000000
--- a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerItem.swift
+++ /dev/null
@@ -1,78 +0,0 @@
-////
-//// TickerItem.swift
-//// ToolinderFeatureCalendarInterface
-////
-//// Created by 송영모 on 2023/09/11.
-////
-//
-//import SwiftUI
-//
-//import ToolinderDomain
-//
-//public struct TickerItem: View {
-// private let ticker: Ticker
-// private let isSelected: Bool
-//
-// private var action: () -> Void
-//
-// public init(
-// ticker: Ticker,
-// isSelected: Bool,
-// action: @escaping () -> Void = {}
-// ) {
-// self.ticker = ticker
-// self.isSelected = isSelected
-// self.action = action
-// }
-//
-// public var body: some View {
-// VStack(spacing: .zero) {
-// HStack {
-// Image(systemName: ticker.type.systemImageName)
-// .font(.body)
-//
-// Text("\(ticker.name) \(ticker.trades?.count ?? 0)")
-// .font(.body)
-// .fontWeight(.semibold)
-// .padding(.trailing)
-//
-// Spacer()
-//
-// Image(systemName: "checkmark.circle")
-// .font(.caption)
-// }
-// .padding(.bottom, 10)
-//
-// HStack(spacing: .zero) {
-// Spacer()
-//
-// Text("++76 ")
-// .font(.caption2)
-// .foregroundStyle(.pink)
-// Text("--59")
-// .font(.caption2)
-// .foregroundStyle(.mint)
-// Text(" 12 vol")
-// .font(.caption2)
-// }
-//
-// HStack(spacing: .zero) {
-// Spacer()
-//
-// Text("(avg) 12,000 \(ticker.currency.rawValue)")
-// .font(.caption2)
-// }
-// }
-// .padding(10)
-// .background(isSelected ? Color(uiColor: .systemGray5) : Color(uiColor: .systemGray6))
-// .clipShape(
-// RoundedRectangle(
-// cornerRadius: 8,
-// style: .continuous
-// )
-// )
-// .onTapGesture {
-// self.action()
-// }
-// }
-//}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/TradeEditStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/EditTradeStore.swift
similarity index 84%
rename from Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/TradeEditStore.swift
rename to Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/EditTradeStore.swift
index b7fd8bc..98e926c 100644
--- a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/TradeEditStore.swift
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/EditTradeStore.swift
@@ -12,7 +12,7 @@ import ComposableArchitecture
import ToolinderDomain
-public struct TradeEditStore: Reducer {
+public struct EditTradeStore: Reducer {
public init() {}
public enum Mode {
@@ -27,7 +27,7 @@ public struct TradeEditStore: Reducer {
public var selectedTrade: Trade?
public var price: Double
- public var volume: Double
+ public var quantity: Double
public var fee: Double
public var selectedDate: Date = .now
public var selectedTradeSide: TradeSide = .buy
@@ -48,7 +48,7 @@ public struct TradeEditStore: Reducer {
self.selectedTicker = selectedTicker
self.selectedTrade = selectedTrade
self.price = selectedTrade?.price ?? 0
- self.volume = selectedTrade?.volume ?? 0
+ self.quantity = selectedTrade?.quantity ?? 0
self.fee = selectedTrade?.fee ?? 0
self.selectedDate = selectedTrade?.date ?? selectedDate
self.note = selectedTrade?.note ?? ""
@@ -60,7 +60,7 @@ public struct TradeEditStore: Reducer {
case onAppear
case setPrice(Double)
- case setVolume(Double)
+ case setQuantity(Double)
case setFee(Double)
case selectDate(Date)
case selectTradeSide(TradeSide)
@@ -98,8 +98,8 @@ public struct TradeEditStore: Reducer {
state.price = price
return .none
- case let .setVolume(volume):
- state.volume = volume
+ case let .setQuantity(volume):
+ state.quantity = volume
return .none
case let .setFee(fee):
@@ -135,7 +135,7 @@ public struct TradeEditStore: Reducer {
trade: state.selectedTrade,
side: state.selectedTradeSide,
price: state.price,
- volume: state.volume,
+ quantity: state.quantity,
fee: state.fee,
images: state.images,
note: state.note,
@@ -172,41 +172,34 @@ public struct TradeEditStore: Reducer {
trade: Trade? = nil,
side: TradeSide,
price: Double,
- volume: Double,
+ quantity: Double,
fee: Double,
images: [Data],
note: String,
date: Date,
ticker: Ticker
- ) -> Effect {
+ ) -> Effect {
guard !price.isZero else { return .none }
- guard !volume.isZero else { return .none }
+ guard !quantity.isZero else { return .none }
guard fee < 100 else { return .none }
+ let tradeDTO = TradeDTO(
+ side: side,
+ price: price,
+ quantity: quantity,
+ fee: fee,
+ images: images,
+ note: note,
+ date: date,
+ ticker: ticker
+ )
+
if let unSavedTrade = trade {
- if let trade = try? tradeClient.updateTrade(unSavedTrade, .init(
- side: side,
- price: price,
- volume: volume,
- fee: fee,
- images: images,
- note: note,
- date: date,
- ticker: ticker
- )).get() {
+ if let trade = try? tradeClient.updateTrade(unSavedTrade, tradeDTO).get() {
return .send(.delegate(.save(trade)))
}
} else {
- if let trade = try? tradeClient.saveTrade(.init(
- side: side,
- price: price,
- volume: volume,
- fee: fee,
- images: images,
- note: note,
- date: date,
- ticker: ticker
- )).get() {
+ if let trade = try? tradeClient.saveTrade(tradeDTO).get() {
return .send(.delegate(.save(trade)))
}
}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/TradeEditView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/EditTradeView.swift
similarity index 83%
rename from Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/TradeEditView.swift
rename to Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/EditTradeView.swift
index c621e53..3b244e4 100644
--- a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/TradeEditView.swift
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/EditTradeView.swift
@@ -14,10 +14,10 @@ import ComposableArchitecture
import ToolinderDomain
import ToolinderShared
-public struct TradeEditView: View {
- let store: StoreOf
+public struct EditTradeView: View {
+ let store: StoreOf
- public init(store: StoreOf) {
+ public init(store: StoreOf) {
self.store = store
}
@@ -46,7 +46,7 @@ public struct TradeEditView: View {
}
}
- private func headerView(viewStore: ViewStoreOf) -> some View {
+ private func headerView(viewStore: ViewStoreOf) -> some View {
HStack {
if viewStore.state.mode == .add {
Button(action: {
@@ -75,9 +75,9 @@ public struct TradeEditView: View {
}
}
- private func pickerView(viewStore: ViewStoreOf) -> some View {
+ private func pickerView(viewStore: ViewStoreOf) -> some View {
HStack {
- Picker("", selection: viewStore.binding(get: \.selectedTradeSide, send: TradeEditStore.Action.selectTradeSide)) {
+ Picker("", selection: viewStore.binding(get: \.selectedTradeSide, send: EditTradeStore.Action.selectTradeSide)) {
ForEach(TradeSide.allCases, id: \.self) { tradeSide in
Text(tradeSide.rawValue)
.tag(tradeSide)
@@ -85,26 +85,26 @@ public struct TradeEditView: View {
}
.pickerStyle(.segmented)
- DatePicker("", selection: viewStore.binding(get: \.selectedDate, send: TradeEditStore.Action.selectDate))
+ DatePicker("", selection: viewStore.binding(get: \.selectedDate, send: EditTradeStore.Action.selectDate))
}
}
- private func inputView(viewStore: ViewStoreOf) -> some View {
+ private func inputView(viewStore: ViewStoreOf) -> some View {
VStack(spacing: 20) {
HStack {
viewStore.state.selectedTicker.currency.image
- TextField("Price", value: viewStore.binding(get: \.price, send: TradeEditStore.Action.setPrice), format: .number)
+ TextField("Price", value: viewStore.binding(get: \.price, send: EditTradeStore.Action.setPrice), format: .number)
.keyboardType(.decimalPad)
Image(systemName: "plusminus.circle.fill")
- TextField("Volume", value: viewStore.binding(get: \.volume, send: TradeEditStore.Action.setVolume), format: .number)
+ TextField("Volume", value: viewStore.binding(get: \.quantity, send: EditTradeStore.Action.setQuantity), format: .number)
.keyboardType(.decimalPad)
Image(systemName: "building.columns.circle.fill")
- TextField("Fee %", value: viewStore.binding(get: \.fee, send: TradeEditStore.Action.setFee), format: .number)
+ TextField("Fee %", value: viewStore.binding(get: \.fee, send: EditTradeStore.Action.setFee), format: .number)
.keyboardType(.decimalPad)
Spacer()
@@ -113,7 +113,7 @@ public struct TradeEditView: View {
VStack(alignment: .leading) {
Image(systemName: "note.text")
- TextEditor(text: viewStore.binding(get: \.note, send: TradeEditStore.Action.setNote))
+ TextEditor(text: viewStore.binding(get: \.note, send: EditTradeStore.Action.setNote))
}
ScrollView(.horizontal) {
@@ -124,7 +124,7 @@ public struct TradeEditView: View {
ImageItem(imageData: imageData)
}
- PhotosPicker(selection: viewStore.binding(get: \.selectedPhotosPickerItems, send: TradeEditStore.Action.setPhotoPickerItems),
+ PhotosPicker(selection: viewStore.binding(get: \.selectedPhotosPickerItems, send: EditTradeStore.Action.setPhotoPickerItems),
matching: .images) {
ImageNewItem()
}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTag/SelectTagStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTag/SelectTagStore.swift
new file mode 100644
index 0000000..bd744ed
--- /dev/null
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTag/SelectTagStore.swift
@@ -0,0 +1,129 @@
+//
+// SelectTagStore.swift
+// ToolinderFeatureTradeInterface
+//
+// Created by 송영모 on 2023/10/01.
+//
+
+import Foundation
+import SwiftUI
+
+import ComposableArchitecture
+
+import ToolinderDomain
+
+public struct SelectTagStore: Reducer {
+ public init() {}
+
+ public struct State: Equatable {
+ public var selectedTags: [Tag]
+
+ public var tagItem: IdentifiedArrayOf = []
+ @PresentationState var editTag: EditTagStore.State?
+
+ public init(selectedTags: [Tag]) {
+ self.selectedTags = selectedTags
+ }
+ }
+
+ public enum Action: Equatable {
+ case onAppear
+
+ case addButtonTapped
+ case confirmButtonTapped
+
+ case fetchTagsRequest
+ case fetchTagsResponse([Tag])
+
+ case tagItem(id: TagItemCellStore.State.ID, action: TagItemCellStore.Action)
+ case editTag(PresentationAction)
+
+ case delegate(Delegate)
+
+ public enum Delegate: Equatable {
+ case select([Tag])
+ case deleted(Tag)
+ }
+ }
+
+ @Dependency(\.tagClient) var tagClient
+
+ public var body: some ReducerOf {
+ Reduce { state, action in
+ switch action {
+ case .onAppear:
+ return .concatenate([
+ .send(.fetchTagsRequest)
+ ])
+
+ case .addButtonTapped:
+ state.editTag = .init(mode: .add)
+ return .none
+
+ case .confirmButtonTapped:
+ return .send(.delegate(.select(state.selectedTags)))
+
+ case .fetchTagsRequest:
+ let tags = (try? tagClient.fetchTags().get()) ?? []
+ return .send(.fetchTagsResponse(tags))
+
+ case let .fetchTagsResponse(tags):
+ state.tagItem = .init(
+ uniqueElements: tags.map { tag in
+ return .init(
+ mode: .edit,
+ tag: tag,
+ isSelected: state.selectedTags.contains(where: { $0 == tag })
+ )
+ }
+ )
+
+ return .none
+
+ case let .tagItem(id: id, action: .delegate(.tapped)):
+ guard let tag = state.tagItem[id: id]?.tag else { return .none }
+
+ state.tagItem[id: id]?.isSelected.toggle()
+ if let index = state.selectedTags.firstIndex(of: tag) {
+ state.selectedTags.remove(at: index)
+ } else {
+ state.selectedTags.append(tag)
+ }
+ return .none
+
+ case let .tagItem(id: id, action: .delegate(.editButtonTapped)):
+ state.editTag = .init(mode: .edit, tag: state.tagItem[id: id]?.tag)
+ return .none
+
+ case .editTag(.presented(.delegate(.save))):
+ state.editTag = nil
+ return .send(.fetchTagsRequest)
+
+ case let .editTag(.presented(.delegate(.delete(tag)))):
+ if let index = state.selectedTags.firstIndex(of: tag) {
+ state.selectedTags.remove(at: index)
+ }
+ state.editTag = nil
+ return .concatenate([
+ .send(.fetchTagsRequest),
+ .send(.delegate(.deleted(tag)))
+ ])
+
+ case .editTag(.dismiss):
+ state.editTag = nil
+ return .none
+
+ default:
+ return .none
+ }
+ }
+
+ .ifLet(\.$editTag, action: /Action.editTag) {
+ EditTagStore()
+ }
+
+ .forEach(\.tagItem, action: /Action.tagItem(id:action:)) {
+ TagItemCellStore()
+ }
+ }
+}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTag/SelectTagView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTag/SelectTagView.swift
new file mode 100644
index 0000000..5ea05d8
--- /dev/null
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTag/SelectTagView.swift
@@ -0,0 +1,77 @@
+//
+// SelectTagView.swift
+// ToolinderFeatureTradeInterface
+//
+// Created by 송영모 on 2023/10/01.
+//
+
+import Foundation
+import SwiftUI
+
+import ComposableArchitecture
+
+import ToolinderDomain
+import ToolinderShared
+
+public struct SelectTagView: View {
+ let store: StoreOf
+
+ public init(store: StoreOf) {
+ self.store = store
+ }
+
+ public var body: some View {
+ WithViewStore(self.store, observe: { $0 }) { viewStore in
+ GeometryReader { proxy in
+ ScrollView {
+ VStack(alignment: .leading) {
+ headerView(viewStore: viewStore)
+ .padding()
+
+ tagItemListView()
+
+ Spacer()
+
+ MinimalButton(title: "Confirm") {
+ viewStore.send(.confirmButtonTapped)
+ }
+ .padding()
+ }
+ .frame(minHeight: proxy.size.height)
+ }
+ }
+ .onAppear {
+ viewStore.send(.onAppear)
+ }
+ .sheet(
+ store: self.store.scope(
+ state: \.$editTag,
+ action: { .editTag($0) }
+ )
+ ) {
+ EditTagView(store: $0)
+ .presentationDetents([.medium])
+ }
+ }
+ }
+
+ @ViewBuilder
+ private func headerView(viewStore: ViewStoreOf) -> some View {
+ EditHeaderView(mode: .select, title: "Tag", isShowNewButton: true) { mode in
+ switch mode {
+ case .new:
+ viewStore.send(.addButtonTapped)
+ default: break
+ }
+ }
+ }
+
+ private func tagItemListView() -> some View {
+ LazyVGrid(columns: .init(repeating: .init(.flexible(minimum: 10, maximum: 500)), count: 3), alignment: .leading, spacing: 10) {
+ ForEachStore(self.store.scope(state: \.tagItem, action: SelectTagStore.Action.tagItem(id:action:))) {
+ TagItemCellView(store: $0)
+ }
+ }
+ .padding(.horizontal)
+ }
+}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTicker/SelectTickerStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTicker/SelectTickerStore.swift
new file mode 100644
index 0000000..dcec6e2
--- /dev/null
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTicker/SelectTickerStore.swift
@@ -0,0 +1,102 @@
+//
+// SelectTickerStore.swift
+// ToolinderFeatureTradeInterface
+//
+// Created by 송영모 on 2023/10/02.
+//
+
+import Foundation
+import SwiftUI
+
+import ComposableArchitecture
+
+import ToolinderDomain
+
+public struct SelectTickerStore: Reducer {
+ public init() {}
+
+ public struct State: Equatable {
+ public var tickerItem: IdentifiedArrayOf = []
+
+ public var selectedTicker: Ticker?
+
+ @PresentationState var editTicker: EditTickerStore.State?
+
+ public init(selectedTicker: Ticker? = nil) {
+ self.selectedTicker = selectedTicker
+ }
+ }
+
+ public enum Action: Equatable {
+ case onAppear
+
+ case addButtonTapped
+
+ case fetchTickersRequest
+ case fetchTickersResponse([Ticker])
+
+ case tickerItem(id: TickerItemCellStore.State.ID, action: TickerItemCellStore.Action)
+ case editTicker(PresentationAction)
+
+ case delegate(Delegate)
+
+ public enum Delegate: Equatable {
+ case select(Ticker)
+ }
+ }
+
+ @Dependency(\.tickerClient) var tickerClient
+
+ public var body: some ReducerOf {
+ Reduce { state, action in
+ switch action {
+ case .onAppear:
+ return .send(.fetchTickersRequest)
+
+ case .addButtonTapped:
+ state.editTicker = .init()
+ return .none
+
+ case .fetchTickersRequest:
+ let tags = (try? tickerClient.fetchTickers().get()) ?? []
+ return .send(.fetchTickersResponse(tags))
+
+ case let .fetchTickersResponse(tickers):
+ state.tickerItem = .init(
+ uniqueElements: tickers.map { ticker in
+ .init(
+ mode: .preview,
+ ticker: ticker,
+ isSelected: state.selectedTicker == ticker
+ )
+ }
+ )
+ return .none
+
+ case let .tickerItem(id: id, action: .delegate(.tapped)):
+ if let ticker = state.tickerItem[id: id]?.ticker {
+ return .send(.delegate(.select(ticker)))
+ } else {
+ return .none
+ }
+
+ case .editTicker(.presented(.delegate(.save))):
+ state.editTicker = nil
+ return .send(.fetchTickersRequest)
+
+ case .editTicker(.dismiss), .editTicker(.presented(.delegate(.cancle))):
+ state.editTicker = nil
+ return .none
+
+ default:
+ return .none
+ }
+ }
+ .ifLet(\.$editTicker, action: /Action.editTicker) {
+ EditTickerStore()
+ }
+ .forEach(\.tickerItem, action: /Action.tickerItem(id:action:)) {
+ TickerItemCellStore()
+ }
+ }
+}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTicker/SelectTickerView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTicker/SelectTickerView.swift
new file mode 100644
index 0000000..18e8439
--- /dev/null
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTicker/SelectTickerView.swift
@@ -0,0 +1,67 @@
+//
+// SelectTicker.swift
+// ToolinderFeatureTradeInterface
+//
+// Created by 송영모 on 2023/10/02.
+//
+
+import SwiftUI
+
+import ComposableArchitecture
+
+import ToolinderShared
+
+public struct SelectTickerView: View {
+ public let store: StoreOf
+
+ public init(store: StoreOf) {
+ self.store = store
+ }
+
+ public var body: some View {
+ WithViewStore(self.store, observe: { $0 }) { viewStore in
+ ScrollView {
+ headerView(viewStore: viewStore)
+ .padding()
+
+ tickerItemListView()
+ .padding()
+ }
+ .onAppear {
+ viewStore.send(.onAppear)
+ }
+ .sheet(
+ store: self.store.scope(
+ state: \.$editTicker,
+ action: { .editTicker($0) }
+ )
+ ) {
+ EditTickerView(store: $0)
+ .presentationDetents([.medium])
+ }
+ }
+ }
+
+ @ViewBuilder
+ private func headerView(viewStore: ViewStoreOf) -> some View {
+ EditHeaderView(
+ mode: .select,
+ title: "Ticker",
+ isShowNewButton: true
+ ) { mode in
+ switch mode {
+ case .new:
+ viewStore.send(.addButtonTapped)
+ default: break
+ }
+ }
+ }
+
+ private func tickerItemListView() -> some View {
+ LazyVGrid(columns: .init(repeating: .init(.flexible(minimum: 10, maximum: 500)), count: 2), alignment: .leading, spacing: 10) {
+ ForEachStore(self.store.scope(state: \.tickerItem, action: SelectTickerStore.Action.tickerItem(id:action:))) {
+ TickerItemCellView(store: $0)
+ }
+ }
+ }
+}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailStore.swift
index 8b6550a..2201ba1 100644
--- a/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailStore.swift
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailStore.swift
@@ -15,11 +15,11 @@ public struct TickerDetailStore: Reducer {
public init() {}
public struct State: Equatable {
- public let ticker: Ticker
+ public var ticker: Ticker
public var tradeDateChartDataEntity: TradeDateChartDataEntity = .init()
- @PresentationState var tickerEdit: TickerEditStore.State?
+ @PresentationState var editTicker: EditTickerStore.State?
public var tradeItem: IdentifiedArrayOf = []
public init(
@@ -37,7 +37,7 @@ public struct TickerDetailStore: Reducer {
case tickerTypeChartDataEntityRequest
case tickerTypeChartDataEntityResponse(TradeDateChartDataEntity)
- case tickerEdit(PresentationAction)
+ case editTicker(PresentationAction)
case tradeItem(id: TradeItemCellStore.State.ID, action: TradeItemCellStore.Action)
case delegate(Delegate)
@@ -61,7 +61,7 @@ public struct TickerDetailStore: Reducer {
])
case .editButtonTapped:
- state.tickerEdit = .init(mode: .edit, selectedTicker: state.ticker)
+ state.editTicker = .init(mode: .edit, ticker: state.ticker)
return .none
case .tickerTypeChartDataEntityRequest:
@@ -78,12 +78,17 @@ public struct TickerDetailStore: Reducer {
state.tradeDateChartDataEntity = entity
return .none
- case .tickerEdit(.presented(.delegate(.delete))):
- state.tickerEdit = nil
+ case let .editTicker(.presented(.delegate(.save(ticker)))):
+ state.editTicker = nil
+ state.ticker = ticker
+ return .none
+
+ case .editTicker(.presented(.delegate(.delete))):
+ state.editTicker = nil
return .send(.delegate(.deleted))
- case .tickerEdit(.dismiss):
- state.tickerEdit = nil
+ case .editTicker(.dismiss):
+ state.editTicker = nil
return .none
default:
@@ -91,8 +96,8 @@ public struct TickerDetailStore: Reducer {
}
}
- .ifLet(\.$tickerEdit, action: /Action.tickerEdit) {
- TickerEditStore()
+ .ifLet(\.$editTicker, action: /Action.editTicker) {
+ EditTickerStore()
}
}
}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailView.swift
index b6a148d..6a99b57 100644
--- a/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailView.swift
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailView.swift
@@ -37,11 +37,11 @@ public struct TickerDetailView: View {
}
.sheet(
store: self.store.scope(
- state: \.$tickerEdit,
- action: { .tickerEdit($0) }
+ state: \.$editTicker,
+ action: { .editTicker($0) }
)
) {
- TickerEditView(store: $0)
+ EditTickerView(store: $0)
.presentationDetents([.medium])
}
.toolbar {
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailStore.swift
index a5060ae..5d08f10 100644
--- a/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailStore.swift
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailStore.swift
@@ -17,7 +17,7 @@ public struct TradeDetailStore: Reducer {
public struct State: Equatable {
public var trade: Trade
- @PresentationState var tradeEdit: TradeEditStore.State?
+ @PresentationState var editTrade: EditTradeStore.State?
public var tradeItem: IdentifiedArrayOf = []
public init(trade: Trade) {
@@ -31,7 +31,7 @@ public struct TradeDetailStore: Reducer {
case editButtonTapped
case newButtonTapped
- case tradeEdit(PresentationAction)
+ case editTrade(PresentationAction)
case tradeItem(id: TradeItemCellStore.State.ID, action: TradeItemCellStore.Action)
case delegate(Delegate)
@@ -56,27 +56,27 @@ public struct TradeDetailStore: Reducer {
case .editButtonTapped:
if let ticker = state.trade.ticker {
- state.tradeEdit = .init(mode: .edit, selectedTicker: ticker, selectedTrade: state.trade)
+ state.editTrade = .init(mode: .edit, selectedTicker: ticker, selectedTrade: state.trade)
}
return .none
case .newButtonTapped:
if let ticker = state.trade.ticker {
- state.tradeEdit = .init(mode: .bypassAdd, selectedTicker: ticker)
+ state.editTrade = .init(mode: .bypassAdd, selectedTicker: ticker)
}
return .none
- case let .tradeEdit(.presented(.delegate(.save(trade)))):
+ case let .editTrade(.presented(.delegate(.save(trade)))):
state.trade = trade
- state.tradeEdit = nil
+ state.editTrade = nil
return .none
- case let .tradeEdit(.presented(.delegate(.delete(trade)))):
- state.tradeEdit = nil
+ case let .editTrade(.presented(.delegate(.delete(trade)))):
+ state.editTrade = nil
return .send(.delegate(.delete(trade)))
- case .tradeEdit(.dismiss), .tradeEdit(.presented(.delegate(.cancel))):
- state.tradeEdit = nil
+ case .editTrade(.dismiss), .editTrade(.presented(.delegate(.cancel))):
+ state.editTrade = nil
return .none
default:
@@ -84,8 +84,8 @@ public struct TradeDetailStore: Reducer {
}
}
- .ifLet(\.$tradeEdit, action: /Action.tradeEdit) {
- TradeEditStore()
+ .ifLet(\.$editTrade, action: /Action.editTrade) {
+ EditTradeStore()
}
}
}
diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailView.swift
index 873a43f..d38eaf5 100644
--- a/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailView.swift
+++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailView.swift
@@ -45,11 +45,11 @@ public struct TradeDetailView: View {
}
.sheet(
store: self.store.scope(
- state: \.$tradeEdit,
- action: { .tradeEdit($0) }
+ state: \.$editTrade,
+ action: { .editTrade($0) }
)
) {
- TradeEditView(store: $0)
+ EditTradeView(store: $0)
.presentationDetents([.medium])
}
.navigationTitle(viewStore.state.trade.ticker?.name ?? "")
@@ -80,7 +80,7 @@ public struct TradeDetailView: View {
HStack(alignment: .bottom, spacing: .zero) {
Spacer()
- Text(scaledString(valueOrNil: viewStore.state.trade.volume))
+ Text(scaledString(valueOrNil: viewStore.state.trade.quantity))
.font(.title)
.fontWeight(.semibold)
diff --git a/Projects/Toolinder/Shared/Util/Resources/Localizable.xcstrings b/Projects/Toolinder/Shared/Util/Resources/Localizable.xcstrings
new file mode 100644
index 0000000..e940975
--- /dev/null
+++ b/Projects/Toolinder/Shared/Util/Resources/Localizable.xcstrings
@@ -0,0 +1,210 @@
+{
+ "sourceLanguage" : "en",
+ "strings" : {
+ "Buy" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "売り"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "매수"
+ }
+ }
+ }
+ },
+ "Currency" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "通貨"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "화폐"
+ }
+ }
+ }
+ },
+ "Edit" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "編集"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "편집"
+ }
+ }
+ }
+ },
+ "History" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "記録"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "기록"
+ }
+ }
+ }
+ },
+ "Name" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "名"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "이름"
+ }
+ }
+ }
+ },
+ "Next" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "次の"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "다음"
+ }
+ }
+ }
+ },
+ "Save" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "保存"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "저장"
+ }
+ }
+ }
+ },
+ "Sell" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "買い"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "매도"
+ }
+ }
+ }
+ },
+ "Summary" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "概略"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "요약"
+ }
+ }
+ }
+ },
+ "Ticker" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "銘柄"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "종목"
+ }
+ }
+ }
+ },
+ "Ticker Type" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "銘柄 種類"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "종목 종류"
+ }
+ }
+ }
+ },
+ "Trade" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "商売"
+ }
+ },
+ "ko" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "거래"
+ }
+ }
+ }
+ }
+ },
+ "version" : "1.0"
+}
\ No newline at end of file
diff --git a/Tuist/Dependencies.swift b/Tuist/Dependencies.swift
index a7eb0cf..9216c6a 100644
--- a/Tuist/Dependencies.swift
+++ b/Tuist/Dependencies.swift
@@ -4,7 +4,9 @@ let dependencies = Dependencies(
swiftPackageManager: [
.remote(url: "https://github.com/googleads/swift-package-manager-google-mobile-ads", requirement: .upToNextMajor(from: "10.9.0")),
.remote(url: "https://github.com/pointfreeco/swift-composable-architecture", requirement: .upToNextMajor(from: "1.2.0")),
- .remote(url: "https://github.com/realm/realm-swift", requirement: .upToNextMajor(from: "10.42.2"))
+ .remote(url: "https://github.com/realm/realm-swift", requirement: .upToNextMajor(from: "10.42.2")),
+ .remote(url: "https://github.com/firebase/firebase-ios-sdk", requirement: .upToNextMajor(from: "10.15.0"))
+
],
platforms: [.iOS]
)