Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
891fa0f016 | |||
77ee3083b3 | |||
c2f540abd8 | |||
b226ad8050 | |||
46be2a0299 | |||
aa55b5463d | |||
519fcc537c | |||
63031172a1 | |||
4875427e24 | |||
f11d3b417a | |||
5a2c237e3a | |||
45a28e386d | |||
48c8f4dfda | |||
450240c364 | |||
966dd4cb34 | |||
fea29d0cb8 | |||
195298dea1 | |||
a825ff927e | |||
655144a2ee | |||
05f11df5e8 | |||
f4233f4562 | |||
4b32aee2a4 | |||
71b2c3389f | |||
![]() |
ca49db680c | ||
![]() |
9ae1fe4265 | ||
3a7d3cbd09 | |||
![]() |
9fb60762c4 | ||
![]() |
875f92c81b | ||
![]() |
c6f224a9d9 | ||
![]() |
f75751670c | ||
![]() |
cb9e906d7c |
|
@ -14,6 +14,7 @@
|
||||||
A78774722CF586AF002FE2EE /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = A78774712CF586AF002FE2EE /* Alamofire */; };
|
A78774722CF586AF002FE2EE /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = A78774712CF586AF002FE2EE /* Alamofire */; };
|
||||||
A7A518CF2CF555E200822D0D /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = A7A518CE2CF555E200822D0D /* README.md */; };
|
A7A518CF2CF555E200822D0D /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = A7A518CE2CF555E200822D0D /* README.md */; };
|
||||||
A7A518D12CF5588500822D0D /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = A7A518D02CF5588500822D0D /* .gitignore */; };
|
A7A518D12CF5588500822D0D /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = A7A518D02CF5588500822D0D /* .gitignore */; };
|
||||||
|
FB0119D32D62EEF000C1FA82 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = FB0119D22D62EEF000C1FA82 /* Starscream */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
@ -53,6 +54,7 @@
|
||||||
A73892252D526A9D00659A62 /* FirebaseCrashlytics in Frameworks */,
|
A73892252D526A9D00659A62 /* FirebaseCrashlytics in Frameworks */,
|
||||||
A771FFF22CFB70D100367DA6 /* KakaoSDK in Frameworks */,
|
A771FFF22CFB70D100367DA6 /* KakaoSDK in Frameworks */,
|
||||||
A73892212D526A9D00659A62 /* FirebaseAnalytics in Frameworks */,
|
A73892212D526A9D00659A62 /* FirebaseAnalytics in Frameworks */,
|
||||||
|
FB0119D32D62EEF000C1FA82 /* Starscream in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -102,6 +104,7 @@
|
||||||
A73892202D526A9D00659A62 /* FirebaseAnalytics */,
|
A73892202D526A9D00659A62 /* FirebaseAnalytics */,
|
||||||
A73892222D526A9D00659A62 /* FirebaseAppCheck */,
|
A73892222D526A9D00659A62 /* FirebaseAppCheck */,
|
||||||
A73892242D526A9D00659A62 /* FirebaseCrashlytics */,
|
A73892242D526A9D00659A62 /* FirebaseCrashlytics */,
|
||||||
|
FB0119D22D62EEF000C1FA82 /* Starscream */,
|
||||||
);
|
);
|
||||||
productName = AcaMate;
|
productName = AcaMate;
|
||||||
productReference = A7A518BB2CF5558B00822D0D /* AcaMate.app */;
|
productReference = A7A518BB2CF5558B00822D0D /* AcaMate.app */;
|
||||||
|
@ -135,6 +138,7 @@
|
||||||
A78774702CF586AF002FE2EE /* XCRemoteSwiftPackageReference "Alamofire" */,
|
A78774702CF586AF002FE2EE /* XCRemoteSwiftPackageReference "Alamofire" */,
|
||||||
A771FFF02CFB70D100367DA6 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */,
|
A771FFF02CFB70D100367DA6 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */,
|
||||||
A738921F2D526A9D00659A62 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
|
A738921F2D526A9D00659A62 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
|
||||||
|
FB0119D12D62EEF000C1FA82 /* XCRemoteSwiftPackageReference "Starscream" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = A7A518BC2CF5558B00822D0D /* Products */;
|
productRefGroup = A7A518BC2CF5558B00822D0D /* Products */;
|
||||||
|
@ -313,7 +317,7 @@
|
||||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent;
|
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -324,7 +328,7 @@
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited) DEV";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited) DEV LOCAL";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
@ -355,7 +359,7 @@
|
||||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent;
|
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -420,6 +424,14 @@
|
||||||
minimumVersion = 5.10.2;
|
minimumVersion = 5.10.2;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
FB0119D12D62EEF000C1FA82 /* XCRemoteSwiftPackageReference "Starscream" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/daltoniam/Starscream";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 4.0.8;
|
||||||
|
};
|
||||||
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
@ -448,6 +460,11 @@
|
||||||
package = A78774702CF586AF002FE2EE /* XCRemoteSwiftPackageReference "Alamofire" */;
|
package = A78774702CF586AF002FE2EE /* XCRemoteSwiftPackageReference "Alamofire" */;
|
||||||
productName = Alamofire;
|
productName = Alamofire;
|
||||||
};
|
};
|
||||||
|
FB0119D22D62EEF000C1FA82 /* Starscream */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = FB0119D12D62EEF000C1FA82 /* XCRemoteSwiftPackageReference "Starscream" */;
|
||||||
|
productName = Starscream;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = A7A518B32CF5558B00822D0D /* Project object */;
|
rootObject = A7A518B32CF5558B00822D0D /* Project object */;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"originHash" : "3b609245b8d633048f6670834279f82d0601cc0879a2d8c9c86fa0dd25734ea3",
|
"originHash" : "2aab34be4ec6f8de8e42f37bee06d0f181ea2ab2db742d0c279bace3d5a0bcfb",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "abseil-cpp-binary",
|
"identity" : "abseil-cpp-binary",
|
||||||
|
@ -127,6 +127,15 @@
|
||||||
"version" : "2.4.0"
|
"version" : "2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "starscream",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/daltoniam/Starscream",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c6bfd1af48efcc9a9ad203665db12375ba6b145a",
|
||||||
|
"version" : "4.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-protobuf",
|
"identity" : "swift-protobuf",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -17,9 +17,6 @@ import FirebaseCore
|
||||||
|
|
||||||
|
|
||||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
printLog("Start Set AppDelegate")
|
printLog("Start Set AppDelegate")
|
||||||
|
|
||||||
|
@ -69,8 +66,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppDelegate: UNUserNotificationCenterDelegate {
|
extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||||
|
|
||||||
//
|
|
||||||
func registerForRemoteNotifications() {
|
func registerForRemoteNotifications() {
|
||||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
|
||||||
print("Permission granted: \(granted)")
|
print("Permission granted: \(granted)")
|
||||||
|
@ -88,7 +83,9 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||||
// 디바이스 토큰 등록 성공 시
|
// 디바이스 토큰 등록 성공 시
|
||||||
func application(_ application: UIApplication,didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
func application(_ application: UIApplication,didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||||
let deviceTokenString = deviceToken.reduce("", {$0 + String(format: "%02.2hX", $1)})
|
let deviceTokenString = deviceToken.reduce("", {$0 + String(format: "%02.2hX", $1)})
|
||||||
printLog("APNs 디바이스 토큰: \(deviceTokenString)")
|
@UserDefault(key: "pushToken", defaultValue: "pushToken") var pushToken
|
||||||
|
pushToken = deviceTokenString
|
||||||
|
printLog("APNs 디바이스 푸시 토큰: \(deviceTokenString)")
|
||||||
// 서버로 디바이스 토큰 전달 로직 추가
|
// 서버로 디바이스 토큰 전달 로직 추가
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +97,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||||
// 앱 켜져있을때 알럿 받으면 직접 로컬로 알림 띄워주는 곳
|
// 앱 켜져있을때 알럿 받으면 직접 로컬로 알림 띄워주는 곳
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
|
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
|
||||||
let userInfo = notification.request.content.userInfo
|
let userInfo = notification.request.content.userInfo
|
||||||
|
printLog(userInfo)
|
||||||
|
|
||||||
if let apsData = userInfo["aps"] as? [AnyHashable: Any],
|
if let apsData = userInfo["aps"] as? [AnyHashable: Any],
|
||||||
let badge = apsData["badge"] as? Int {
|
let badge = apsData["badge"] as? Int {
|
||||||
|
@ -111,6 +109,18 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let bid = userInfo["bid"] as? String {
|
||||||
|
printLog("bid = \(bid)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let content = userInfo["content"] as? String {
|
||||||
|
printLog("content = \(content)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let pid = userInfo["pid"] as? String {
|
||||||
|
printLog("pid = \(pid)")
|
||||||
|
}
|
||||||
|
|
||||||
if #available(iOS 14.0, *) {
|
if #available(iOS 14.0, *) {
|
||||||
return [[.list,.banner,.sound]]
|
return [[.list,.banner,.sound]]
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -7,15 +7,41 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
// MARK: - ACAMATE
|
// MARK: - ACAMATE
|
||||||
// APPSTORE_URL : https://apps.apple.com/us/app/%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8%EB%A9%94%EC%9D%B4%ED%8A%B8/id6739448113
|
// APPSTORE_URL : https://apps.apple.com/us/app/%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8%EB%A9%94%EC%9D%B4%ED%8A%B8/id6739448113
|
||||||
//#if DEV && LOCAL
|
/// 주의사항
|
||||||
|
/// plist 에서 http 설정 걸려있는거 지워야 함
|
||||||
|
#if LOCAL
|
||||||
|
public let API_URL: String = "http://10.149.217.64:5144"
|
||||||
//public let API_URL: String = "http://localhost:5144"
|
//public let API_URL: String = "http://localhost:5144"
|
||||||
//#else
|
//public let API_URL: String = "https://localhost:7086"
|
||||||
#if DEV
|
public let WS_URL: String = "ws://localhost:5144"
|
||||||
|
|
||||||
|
/// 회사 맥에서 사용할 경우의 URL
|
||||||
|
//public let WS_URL: String = "ws://10.149.217.64:5144"
|
||||||
|
|
||||||
|
/// 집 맥에서 사용할 경우의 URL
|
||||||
|
//public let WS_URL: String = "ws://192.168.0.71:5144"
|
||||||
|
//ipconfig getifaddr en0 이거는 와이파이 주소 알아내는거임
|
||||||
|
|
||||||
|
#elseif DEV
|
||||||
public let API_URL: String = "https://devacamate.ipstein.myds.me"
|
public let API_URL: String = "https://devacamate.ipstein.myds.me"
|
||||||
|
/// 서버용 웹소켓
|
||||||
|
//public let WS_URL: String = "ws://ipstein.myds.me:7004"
|
||||||
|
|
||||||
|
/// 회사 맥에서 사용할 경우의 URL
|
||||||
|
public let WS_URL: String = "ws://10.149.217.64:5144"
|
||||||
|
|
||||||
|
/// 집 맥에서 사용할 경우의 URL
|
||||||
|
//public let WS_URL: String = "ws://192.168.0.71:5144"
|
||||||
|
//ipconfig getifaddr en0 이거는 와이파이 주소 알아내는거임
|
||||||
|
|
||||||
#else
|
#else
|
||||||
public let API_URL: String = "https://acamate.ipstein.myds.me"
|
public let API_URL: String = "https://acamate.ipstein.myds.me"
|
||||||
|
public let WS_URL: String = "wss://acamate.ipstein.myds.me"
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
public let API_HEADER = "iOS_AM_Connect_Key"
|
||||||
|
|
||||||
|
|
||||||
// MARK: - TYPEALIAS
|
// MARK: - TYPEALIAS
|
||||||
typealias VOID_TO_VOID = () -> ()
|
typealias VOID_TO_VOID = () -> ()
|
||||||
|
|
109
AcaMate/0. Setup/WebView.swift
Normal file
109
AcaMate/0. Setup/WebView.swift
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
//
|
||||||
|
// WebView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 3/24/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import WebKit
|
||||||
|
|
||||||
|
struct WebView: UIViewControllerRepresentable {
|
||||||
|
|
||||||
|
|
||||||
|
var url: URL
|
||||||
|
@Binding var showWebView: Bool
|
||||||
|
@Binding var isLoading: Bool
|
||||||
|
var complete: ((Any) -> Void)?
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
return Coordinator(parent: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
|
||||||
|
var parent: WebView
|
||||||
|
|
||||||
|
init(parent: WebView) {
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||||
|
print("웹뷰 로딩 완료")
|
||||||
|
parent.isLoading = false
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func userContentController(_ userContentController: WKUserContentController,
|
||||||
|
didReceive message: WKScriptMessage) {
|
||||||
|
if let data = message.body as? [String: Any]
|
||||||
|
{
|
||||||
|
if let road = data["roadAddress"], let code = data["zonecode"] {
|
||||||
|
printLog("도로명 주소: \(road) // ZIP CODE: \(code)")
|
||||||
|
parent.complete?((road,code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent.showWebView.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: WebViewController, context: Context) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> WebViewController {
|
||||||
|
return WebViewController(url: url, isLoading: $isLoading,
|
||||||
|
complete: complete, coordinator: context.coordinator)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebViewController: UIViewController, WKUIDelegate {
|
||||||
|
|
||||||
|
var url: URL
|
||||||
|
@Binding var isLoading: Bool
|
||||||
|
var complete: ((Any) -> Void)?
|
||||||
|
var coordinator: WebView.Coordinator
|
||||||
|
|
||||||
|
init(url: URL, isLoading: Binding<Bool>, complete: ((Any) -> Void)?, coordinator: WebView.Coordinator) {
|
||||||
|
self.url = url
|
||||||
|
_isLoading = isLoading
|
||||||
|
self.complete = complete
|
||||||
|
self.coordinator = coordinator
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
webView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func webView() {
|
||||||
|
let request = URLRequest(url: url)
|
||||||
|
|
||||||
|
let configuration = WKWebViewConfiguration()
|
||||||
|
let contentController = WKUserContentController()
|
||||||
|
|
||||||
|
contentController.add(coordinator, name: "callBackHandler")
|
||||||
|
configuration.userContentController = contentController
|
||||||
|
|
||||||
|
let webview = WKWebView(frame: view.bounds, configuration: configuration)
|
||||||
|
webview.uiDelegate = self
|
||||||
|
webview.navigationDelegate = coordinator
|
||||||
|
webview.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
|
||||||
|
webview.isUserInteractionEnabled = true
|
||||||
|
webview.scrollView.isUserInteractionEnabled = true
|
||||||
|
webview.scrollView.delaysContentTouches = false
|
||||||
|
|
||||||
|
webview.load(request)
|
||||||
|
view.addSubview(webview)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,11 +20,6 @@ struct BoxBtnView<Content: View>: View {
|
||||||
action()
|
action()
|
||||||
} label: {
|
} label: {
|
||||||
content
|
content
|
||||||
// if let image = image {
|
|
||||||
// image
|
|
||||||
// .resizable()
|
|
||||||
// .frame(width: width, height: height)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,18 +35,17 @@ struct CircleBtnView: View {
|
||||||
.foregroundStyle(state.foreColor)
|
.foregroundStyle(state.foreColor)
|
||||||
.frame(width: state.width/2, height: state.height/2)
|
.frame(width: state.width/2, height: state.height/2)
|
||||||
}
|
}
|
||||||
if let title = state.title, let font = state.font {
|
if let title = state.text, let font = state.font {
|
||||||
Text("\(title)")
|
Text("\(title)")
|
||||||
.font(font)
|
.font(font)
|
||||||
.lineLimit(1)
|
|
||||||
.minimumScaleFactor(0.5)
|
|
||||||
.truncationMode(.tail)
|
|
||||||
.foregroundStyle(state.foreColor)
|
.foregroundStyle(state.foreColor)
|
||||||
|
.multilineStyle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: state.width, height: state.height)
|
.frame(width: state.width, height: state.height)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
endTextEditing()
|
||||||
guard let action = state.action else {return}
|
guard let action = state.action else {return}
|
||||||
action()
|
action()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,17 +15,15 @@ struct SimpleBtnView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if let state = vm.btnStates[id] {
|
if let state = vm.btnStates[id] {
|
||||||
if let title = state.title, let font = state.font {
|
if let title = state.text, let font = state.font {
|
||||||
Text("\(title)")
|
Text("\(title)")
|
||||||
.font(font)
|
.font(font)
|
||||||
.lineLimit(1)
|
.multilineStyle(.center)
|
||||||
.minimumScaleFactor(0.5)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.truncationMode(.tail)
|
|
||||||
.foregroundStyle(state.textColor)
|
.foregroundStyle(state.textColor)
|
||||||
.frame(width: state.width, height: state.height)
|
.frame(width: state.width, height: state.height)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if state.isUsable {
|
if state.isUsable {
|
||||||
|
endTextEditing()
|
||||||
guard let action = state.action else { return }
|
guard let action = state.action else { return }
|
||||||
action()
|
action()
|
||||||
}
|
}
|
||||||
|
@ -33,6 +31,7 @@ struct SimpleBtnView: View {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Button{
|
Button{
|
||||||
|
endTextEditing()
|
||||||
guard let action = state.action else { return }
|
guard let action = state.action else { return }
|
||||||
action()
|
action()
|
||||||
} label: {
|
} label: {
|
||||||
|
|
165
AcaMate/1. View/10. Common/DropDownView.swift
Normal file
165
AcaMate/1. View/10. Common/DropDownView.swift
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
//
|
||||||
|
// DropDownView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 3/25/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class DropdownManager: ObservableObject {
|
||||||
|
@Published var isPresented = false
|
||||||
|
@Published var items: [String] = []
|
||||||
|
@Published var anchor: CGRect = .zero
|
||||||
|
@Published var font: Font = .body
|
||||||
|
|
||||||
|
var onSelect: ((Int) -> Void)?
|
||||||
|
|
||||||
|
func show(items: [String], anchor: CGRect,
|
||||||
|
onSelect: @escaping (Int) -> Void) {
|
||||||
|
self.items = items
|
||||||
|
self.anchor = anchor
|
||||||
|
self.onSelect = onSelect
|
||||||
|
isPresented = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismiss() {
|
||||||
|
isPresented = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// .coordinateSpace(name: "dropdownArea") 이거 지정을 해야 함
|
||||||
|
/// GlobalDropdownOverlay(manager: dropdownManager) 이거도 ZStack으로 넣어야 함
|
||||||
|
struct DropdownButton: View {
|
||||||
|
@ObservedObject var manager: DropdownManager
|
||||||
|
@State var title: String
|
||||||
|
|
||||||
|
var onSelect: ((Int) -> Void)?
|
||||||
|
let items: [String]
|
||||||
|
|
||||||
|
|
||||||
|
init(manager: DropdownManager, title: String, items: [String],
|
||||||
|
onSelect: @escaping (Int) -> Void) {
|
||||||
|
self.manager = manager
|
||||||
|
self.title = title
|
||||||
|
self.items = items
|
||||||
|
self.onSelect = onSelect
|
||||||
|
}
|
||||||
|
|
||||||
|
@State private var frame: CGRect = .zero
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button {
|
||||||
|
endTextEditing()
|
||||||
|
manager.show(items: items, anchor: frame) { index in
|
||||||
|
self.onSelect?(index)
|
||||||
|
print("선택한 항목: \(items[index])")
|
||||||
|
title = items[index]
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
Spacer()
|
||||||
|
Text(title)
|
||||||
|
.font(manager.font)
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.5)
|
||||||
|
Image(systemName: /*manager.isPresented ? "chevron.up" : */ "chevron.down")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 8, height: 4)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(4)
|
||||||
|
.background{
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color.clear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.background(
|
||||||
|
GeometryReader { geo in
|
||||||
|
Color.clear
|
||||||
|
.onAppear {
|
||||||
|
frame = geo.frame(in: .named("dropdownArea"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct GlobalDropdownOverlay: View {
|
||||||
|
@ObservedObject var manager: DropdownManager
|
||||||
|
|
||||||
|
@State private var scrollOffset: CGPoint = .zero
|
||||||
|
|
||||||
|
private var maxVisibleItemCount: Int {
|
||||||
|
if manager.items.count > 5 {
|
||||||
|
return 5
|
||||||
|
} else { return manager.items.count }
|
||||||
|
}
|
||||||
|
private let itemHeight: CGFloat = 42
|
||||||
|
|
||||||
|
private var heightForDropBox: CGFloat {
|
||||||
|
CGFloat(maxVisibleItemCount) * itemHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if manager.isPresented {
|
||||||
|
ZStack(alignment: .topLeading) {
|
||||||
|
Color.black.opacity(0.1)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onTapGesture {
|
||||||
|
manager.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
if manager.items.count > maxVisibleItemCount {
|
||||||
|
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { _ in
|
||||||
|
dropdownContent()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dropdownContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: manager.anchor.width, height: heightForDropBox)
|
||||||
|
.background{
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color(.Normal.normal))
|
||||||
|
.stroke(Color(.Second.normal))
|
||||||
|
.strokeBorder(lineWidth: 2)
|
||||||
|
}
|
||||||
|
.position(x: manager.anchor.midX,
|
||||||
|
y: manager.anchor.maxY+heightForDropBox/2)
|
||||||
|
.animation(.easeOut(duration: 0.25), value: manager.isPresented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func dropdownContent() -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
ForEach(Array(manager.items.enumerated()), id: \.offset) { index, item in
|
||||||
|
Button {
|
||||||
|
manager.onSelect?(index)
|
||||||
|
manager.dismiss()
|
||||||
|
} label: {
|
||||||
|
Text(item)
|
||||||
|
.font(manager.font)
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.4)
|
||||||
|
.padding([.leading, .trailing], 8)
|
||||||
|
.frame(maxWidth: .infinity, minHeight: 40, maxHeight: 40, alignment: .center)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
if index < manager.items.count - 1 {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color(.Second.normal).opacity(0.3))
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,26 +12,31 @@ import Combine
|
||||||
/// 이거 위에 다른 뷰들이 위젯 느낌으로 계속 갈아 끼워지는거
|
/// 이거 위에 다른 뷰들이 위젯 느낌으로 계속 갈아 끼워지는거
|
||||||
struct NavigationView: View {
|
struct NavigationView: View {
|
||||||
@EnvironmentObject var appVM: AppViewModel
|
@EnvironmentObject var appVM: AppViewModel
|
||||||
@State private var naviState : NaviState = .init(act: .NONE, path: .Intro)
|
|
||||||
@State private var history: [PathName] = [.Intro]
|
@State private var history: [PathName] = [.Intro]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
switch naviState.path {
|
switch appVM.naviState.path {
|
||||||
case .NONE:
|
case .NONE:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
case .Intro:
|
case .Intro:
|
||||||
IntroView(naviState: $naviState)
|
IntroView(appVM)
|
||||||
case .Login :
|
case .Login :
|
||||||
LoginView(naviState: $naviState)
|
LoginView(appVM)
|
||||||
|
case .Register(let type, let id):
|
||||||
|
RegisterView(appVM, type: type, snsID: id)
|
||||||
|
case .SelectAcademy:
|
||||||
|
SelectAcademyView(appVM)
|
||||||
case .Main:
|
case .Main:
|
||||||
MainView(naviState: $naviState)
|
MainView()
|
||||||
|
case .ChatRoom(let id):
|
||||||
|
ChattingRoomView(roomID: id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: naviState) { old, new in
|
.onChange(of: appVM.naviState) { old, new in
|
||||||
switch new.act {
|
switch new.act {
|
||||||
case .NONE:
|
case .NONE:
|
||||||
break
|
break
|
||||||
|
@ -54,6 +59,7 @@ struct NavigationView: View {
|
||||||
.setAlert()
|
.setAlert()
|
||||||
.setNetwork()
|
.setNetwork()
|
||||||
.loadingView(isLoading: $appVM.isLoading)
|
.loadingView(isLoading: $appVM.isLoading)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 경로에 한 단계 추가
|
/// 경로에 한 단계 추가
|
||||||
|
@ -64,7 +70,8 @@ struct NavigationView: View {
|
||||||
/// 가장 가까운 경로 삭제
|
/// 가장 가까운 경로 삭제
|
||||||
private func popHistory() {
|
private func popHistory() {
|
||||||
history.removeLast()
|
history.removeLast()
|
||||||
naviState.set(act: .NONE, path: history.last ?? .NONE)
|
appVM.naviState.set(act: .NONE, path: history.last ?? .NONE)
|
||||||
|
// naviState.set(act: .NONE, path: history.last ?? .NONE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 경로 기록 전체 삭제
|
/// 경로 기록 전체 삭제
|
||||||
|
@ -76,17 +83,20 @@ struct NavigationView: View {
|
||||||
/// 경로의 최상단 지우고, 새로 이동하는 경로로 설정
|
/// 경로의 최상단 지우고, 새로 이동하는 경로로 설정
|
||||||
private func moveHistory(path: PathName) {
|
private func moveHistory(path: PathName) {
|
||||||
if path == .NONE {
|
if path == .NONE {
|
||||||
naviState.set(act: .RESET, path: history.first ?? .Main)
|
appVM.naviState.set(act: .RESET, path: history.first ?? .Main)
|
||||||
|
// naviState.set(act: .RESET, path: history.first ?? .Main)
|
||||||
}
|
}
|
||||||
if history.contains(path) {
|
if history.contains(path) {
|
||||||
let remove = history.count - history.firstIndex(of: path)! - 1
|
let remove = history.count - history.firstIndex(of: path)! - 1
|
||||||
history.removeLast(remove)
|
history.removeLast(remove)
|
||||||
if remove > 0 {
|
if remove > 0 {
|
||||||
naviState.set(act: .NONE, path: path)
|
appVM.naviState.set(act: .NONE, path: path)
|
||||||
|
// naviState.set(act: .NONE, path: path)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
naviState.set(act: .RESET, path: path)
|
appVM.naviState.set(act: .RESET, path: path)
|
||||||
|
// naviState.set(act: .RESET, path: path)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showHistory() {
|
private func showHistory() {
|
||||||
|
|
|
@ -1,117 +1,117 @@
|
||||||
|
////
|
||||||
|
//// AccountLoginView.swift
|
||||||
|
//// AcaMate
|
||||||
|
////
|
||||||
|
//// Created by Sean Kim on 12/14/24.
|
||||||
|
////
|
||||||
//
|
//
|
||||||
// AccountLoginView.swift
|
//import SwiftUI
|
||||||
// AcaMate
|
|
||||||
//
|
//
|
||||||
// Created by Sean Kim on 12/14/24.
|
//struct AccountLoginView: View {
|
||||||
//
|
// @ObservedObject var loginVM.: LoginViewModel
|
||||||
|
// @Binding var userId: String
|
||||||
import SwiftUI
|
// @Binding var password: String
|
||||||
|
// @Binding var isSecure: Bool
|
||||||
struct AccountLoginView: View {
|
// @Binding var isSave: Bool
|
||||||
@ObservedObject var viewModel: LoginViewModel
|
// var body: some View {
|
||||||
@Binding var userId: String
|
// VStack(spacing: 0) {
|
||||||
@Binding var password: String
|
// ZStack(alignment: .leading) {
|
||||||
@Binding var isSecure: Bool
|
// if userId.isEmpty {
|
||||||
@Binding var isSave: Bool
|
// Text("아이디를 입력하세요.")
|
||||||
var body: some View {
|
// .font(.nps(font: .regular, size: 16))
|
||||||
VStack(spacing: 0) {
|
// .foregroundStyle(Color(.Text.border))
|
||||||
ZStack(alignment: .leading) {
|
// .padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||||
if userId.isEmpty {
|
// }
|
||||||
Text("아이디를 입력하세요.")
|
// CustomTextField(placeholder: "", text: $userId)
|
||||||
.font(.nps(font: .regular, size: 16))
|
// .frame(maxWidth: .infinity,maxHeight: 24)
|
||||||
.foregroundStyle(Color(.Text.border))
|
// .padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||||
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
// }
|
||||||
}
|
// .background {
|
||||||
CustomTextField(placeholder: "", text: $userId)
|
// RoundedRectangle(cornerRadius: 24)
|
||||||
.frame(maxWidth: .infinity,maxHeight: 24)
|
// .foregroundStyle(.white)
|
||||||
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
// }
|
||||||
}
|
// .padding(EdgeInsets(top: 0, leading: 12, bottom: 8, trailing: 12))
|
||||||
.background {
|
//
|
||||||
RoundedRectangle(cornerRadius: 24)
|
// ZStack(alignment: .leading) {
|
||||||
.foregroundStyle(.white)
|
// if password.isEmpty {
|
||||||
}
|
// Text("비밀번호를 입력하세요.")
|
||||||
.padding(EdgeInsets(top: 0, leading: 12, bottom: 8, trailing: 12))
|
// .font(.nps(font: .regular, size: 16))
|
||||||
|
// .foregroundStyle(Color(.Text.border))
|
||||||
ZStack(alignment: .leading) {
|
// .padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||||
if password.isEmpty {
|
// }
|
||||||
Text("비밀번호를 입력하세요.")
|
// CustomTextField(placeholder: "", text: $password, isSecure: $isSecure)
|
||||||
.font(.nps(font: .regular, size: 16))
|
// .frame(maxWidth: .infinity,maxHeight: 24)
|
||||||
.foregroundStyle(Color(.Text.border))
|
// .padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||||
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
//
|
||||||
}
|
// HStack {
|
||||||
CustomTextField(placeholder: "", text: $password, isSecure: $isSecure)
|
// Spacer()
|
||||||
.frame(maxWidth: .infinity,maxHeight: 24)
|
// Button {
|
||||||
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
// isSecure.toggle()
|
||||||
|
// } label: {
|
||||||
HStack {
|
// if password.isEmpty {
|
||||||
Spacer()
|
// Rectangle()
|
||||||
Button {
|
// .frame(width: 16, height: 2)
|
||||||
isSecure.toggle()
|
// .foregroundStyle(Color(.Text.border))
|
||||||
} label: {
|
// .padding(.trailing,24)
|
||||||
if password.isEmpty {
|
// }
|
||||||
Rectangle()
|
// else {
|
||||||
.frame(width: 16, height: 2)
|
// if isSecure {
|
||||||
.foregroundStyle(Color(.Text.border))
|
// Image(systemName: "eye")
|
||||||
.padding(.trailing,24)
|
// .frame(width: 16, height: 16)
|
||||||
}
|
// .foregroundStyle(Color(.Text.detail))
|
||||||
else {
|
// .padding(.trailing,24)
|
||||||
if isSecure {
|
// }
|
||||||
Image(systemName: "eye")
|
// else {
|
||||||
.frame(width: 16, height: 16)
|
// Image(systemName: "eye.slash")
|
||||||
.foregroundStyle(Color(.Text.detail))
|
// .frame(width: 16, height: 16)
|
||||||
.padding(.trailing,24)
|
// .foregroundStyle(Color(.Text.detail))
|
||||||
}
|
// .padding(.trailing,24)
|
||||||
else {
|
//
|
||||||
Image(systemName: "eye.slash")
|
// }
|
||||||
.frame(width: 16, height: 16)
|
// }
|
||||||
.foregroundStyle(Color(.Text.detail))
|
// }
|
||||||
.padding(.trailing,24)
|
// }
|
||||||
|
// }
|
||||||
}
|
// .background {
|
||||||
}
|
// RoundedRectangle(cornerRadius: 24)
|
||||||
}
|
// .foregroundStyle(.white)
|
||||||
}
|
// }
|
||||||
}
|
// .padding(EdgeInsets(top: 0, leading: 12, bottom: 8, trailing: 12))
|
||||||
.background {
|
//
|
||||||
RoundedRectangle(cornerRadius: 24)
|
// Button {
|
||||||
.foregroundStyle(.white)
|
// isSave.toggle()
|
||||||
}
|
// } label: {
|
||||||
.padding(EdgeInsets(top: 0, leading: 12, bottom: 8, trailing: 12))
|
// HStack(alignment: .center, spacing: 4) {
|
||||||
|
// Spacer(minLength: 1)
|
||||||
Button {
|
// if isSave {
|
||||||
isSave.toggle()
|
// Image(systemName: "checkmark.square")
|
||||||
} label: {
|
// .foregroundStyle(Color(.Second.normal))
|
||||||
HStack(alignment: .center, spacing: 4) {
|
// .frame(width: 24, height: 24)
|
||||||
Spacer(minLength: 1)
|
// } else {
|
||||||
if isSave {
|
// Image(systemName: "square")
|
||||||
Image(systemName: "checkmark.square")
|
// .foregroundStyle(Color(.Second.normal))
|
||||||
.foregroundStyle(Color(.Second.normal))
|
// .frame(width: 24, height: 24)
|
||||||
.frame(width: 24, height: 24)
|
// }
|
||||||
} else {
|
//
|
||||||
Image(systemName: "square")
|
// Text("로그인 정보 저장")
|
||||||
.foregroundStyle(Color(.Second.normal))
|
// .font(.nps(font: .regular, size: 16))
|
||||||
.frame(width: 24, height: 24)
|
// .foregroundStyle(Color(.Text.detail))
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
Text("로그인 정보 저장")
|
// .padding(EdgeInsets(top: 0, leading: 0, bottom: 24, trailing: 12))
|
||||||
.font(.nps(font: .regular, size: 16))
|
//
|
||||||
.foregroundStyle(Color(.Text.detail))
|
// Button {
|
||||||
}
|
// loginVM..loginAction.send(true)
|
||||||
}
|
// } label: {
|
||||||
.padding(EdgeInsets(top: 0, leading: 0, bottom: 24, trailing: 12))
|
// Text("로그인")
|
||||||
|
// .font(.nps(font: .bold, size: 24))
|
||||||
Button {
|
// .foregroundStyle(Color(.Text.white))
|
||||||
viewModel.loginAction.send(true)
|
// .padding(EdgeInsets(top: 8, leading: 48, bottom: 8, trailing: 48))
|
||||||
} label: {
|
// .background{
|
||||||
Text("로그인")
|
// RoundedRectangle(cornerRadius: 12)
|
||||||
.font(.nps(font: .bold, size: 24))
|
// .foregroundStyle(Color(.Second.normal))
|
||||||
.foregroundStyle(Color(.Text.white))
|
// }
|
||||||
.padding(EdgeInsets(top: 8, leading: 48, bottom: 8, trailing: 48))
|
// }
|
||||||
.background{
|
// }
|
||||||
RoundedRectangle(cornerRadius: 12)
|
// }
|
||||||
.foregroundStyle(Color(.Second.normal))
|
//}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,17 +8,21 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
|
|
||||||
struct IntroView: View {
|
struct IntroView: View {
|
||||||
@EnvironmentObject var appVM: AppViewModel
|
@EnvironmentObject var appVM: AppViewModel
|
||||||
|
@StateObject var introVM: IntroViewModel
|
||||||
@State var cancellables: Set<AnyCancellable> = []
|
@State var cancellables: Set<AnyCancellable> = []
|
||||||
@Binding var naviState : NaviState
|
|
||||||
|
init(_ appVM: AppViewModel) {
|
||||||
|
_introVM = StateObject(wrappedValue: IntroViewModel(appVM))
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 100)
|
.frame(height: 100)
|
||||||
Image(.Logo.appIcon)
|
Image(.Logo.crystalIcon)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 200, height: 200)
|
.frame(width: 200, height: 200)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -39,92 +43,7 @@ struct IntroView: View {
|
||||||
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
printLog("IntroView_onAppear")
|
printLog("IntroView_onAppear")
|
||||||
#if LOCAL
|
introVM.appStart()
|
||||||
naviState.set(act: .RESET, path: .Login)
|
|
||||||
#else
|
|
||||||
subscribeAlertAction()
|
|
||||||
loadVersion()
|
|
||||||
.sink { completion in
|
|
||||||
switch completion {
|
|
||||||
case .failure(let error):
|
|
||||||
printLog(error)
|
|
||||||
case .finished: break
|
|
||||||
}
|
|
||||||
} receiveValue: { version in
|
|
||||||
let compareForce = compareVersion(version.force_ver, currentVersion())
|
|
||||||
let compareChoice = compareVersion(version.final_ver, currentVersion())
|
|
||||||
|
|
||||||
if compareForce == .bigger {
|
|
||||||
appVM.alertData = SetAlertData().setForceUpdate(
|
|
||||||
action: appVM.alertAction
|
|
||||||
)
|
|
||||||
appVM.showAlert.toggle()
|
|
||||||
} else if compareChoice == .bigger && version.choice_update_yn {
|
|
||||||
appVM.alertData = SetAlertData().setSelectUpdate(
|
|
||||||
action: appVM.alertAction
|
|
||||||
)
|
|
||||||
appVM.showAlert.toggle()
|
|
||||||
} else {
|
|
||||||
naviState.set(act: .RESET, path: .Login)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func subscribeAlertAction() {
|
|
||||||
appVM.alertAction
|
|
||||||
.compactMap { $0 }
|
|
||||||
.sink { action in
|
|
||||||
if action == "updateNow" {
|
|
||||||
exit(1)
|
|
||||||
//MARK: - TODO (앱스토어 이동 로직 넣을 것)
|
|
||||||
} else {
|
|
||||||
naviState.set(act: .RESET, path: .Login)
|
|
||||||
}
|
|
||||||
}.store(in: &cancellables)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private func loadVersion() -> Future<VersionData, Error> {
|
|
||||||
return Future { promise in
|
|
||||||
loadAPIData(url: "\(API_URL)",
|
|
||||||
path: "/api/v1/in/app/version",
|
|
||||||
parameters: ["type":"I"],
|
|
||||||
decodingType: APIResponse<VersionData>.self)
|
|
||||||
.sink { completion in
|
|
||||||
switch completion {
|
|
||||||
case .failure(let error):
|
|
||||||
printLog("\(error)")
|
|
||||||
promise(.failure(error))
|
|
||||||
case .finished: break
|
|
||||||
}
|
|
||||||
} receiveValue: { data in
|
|
||||||
guard let apiData = data as? APIResponse<VersionData> else {return}
|
|
||||||
printLog("\(apiData.data.toStringDict())")
|
|
||||||
promise(.success(apiData.data))
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private func currentVersion() -> String {
|
|
||||||
guard let currentVer = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { return "" }
|
|
||||||
return currentVer
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private func versionChange(ver: String) -> [Int] {
|
|
||||||
return ver.components(separatedBy: ["."]).map {Int($0) ?? 0}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#Preview {
|
|
||||||
// IntroView(path: $NavigationPath())
|
|
||||||
//}
|
|
||||||
|
|
|
@ -8,110 +8,88 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
|
|
||||||
struct LoginView: View {
|
struct LoginView: View {
|
||||||
@EnvironmentObject var appVM: AppViewModel
|
@EnvironmentObject var appVM: AppViewModel
|
||||||
@StateObject private var loginVM = LoginViewModel()
|
@StateObject var loginVM: LoginViewModel
|
||||||
@State var cancellables: Set<AnyCancellable> = []
|
|
||||||
@Binding var naviState : NaviState
|
|
||||||
|
|
||||||
@State var selectIdLogin: Bool = false
|
init(_ appVM: AppViewModel) {
|
||||||
|
_loginVM = StateObject(wrappedValue: LoginViewModel(appVM))
|
||||||
@State var userId: String = ""
|
}
|
||||||
@State var password: String = ""
|
|
||||||
@State var isSecure: Bool = true
|
|
||||||
@State var isSave: Bool = false
|
|
||||||
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Spacer().frame(height: 100)
|
Spacer().frame(height: 100)
|
||||||
Image(.Logo.appIcon)
|
Image(.Logo.crystalIcon)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 200, height: 200)
|
.frame(width: 200, height: 200)
|
||||||
// .padding(.top, 80)
|
// .padding(.top, 80)
|
||||||
.padding(.bottom, 84)
|
.padding(.bottom, 84)
|
||||||
|
|
||||||
/// 앱 아이콘 이미지
|
/// 앱 아이콘 이미지
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
Button {
|
Button {
|
||||||
// MARK: - TODO, 카카오 계정 로그인 구현
|
// MARK: - TODO, 카카오 계정 로그인 구현
|
||||||
appVM.isLoading.toggle()
|
// loginVM.toggleLoading = true
|
||||||
loginAction(type: .Kakao)
|
loginVM.loginAction(type: .Kakao)
|
||||||
|
|
||||||
} label: {
|
} label: {
|
||||||
makeButton(image: Image(.Logo.kakaoIcon),color: Color(.Other.yellow), "카카오 계정으로 시작하기")
|
makeButton(image: Image(.Logo.kakaoIcon),color: Color(.Other.yellow), "카카오 계정으로 시작하기")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
// MARK: - TODO, 애플 계정 로그인 구현
|
// MARK: - TODO, 애플 계정 로그인 구현
|
||||||
naviState.set(act: .MOVE, path: .Main)
|
// appVM.naviState.set(act: .ADD, path: .SelectAcademy(bids: ["AA0000", "AA0001"]))
|
||||||
|
// loginVM.toggleLoading = true
|
||||||
|
// loginVM.loginTest(type: .Kakao, id: "TestSNSID1@#")
|
||||||
|
// loginVM.USERPAITEST()
|
||||||
|
|
||||||
|
|
||||||
} label: {
|
} label: {
|
||||||
makeButton(image: Image(.Logo.appleIcon), color: Color(.Text.black), "애플 계정으로 시작하기")
|
makeButton(image: Image(.Logo.appleIcon), color: Color(.Text.black), "애플 계정으로 시작하기")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CustomTxfView(placeholder: "id",
|
||||||
|
text: $loginVM.devId,
|
||||||
|
alignment: .center,
|
||||||
|
font: .nps(size: 12))
|
||||||
|
.frame(maxWidth: .infinity,maxHeight: 48)
|
||||||
|
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.foregroundStyle(Color(.Normal.light))
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
// loginVM.toggleLoading = true
|
||||||
|
loginVM.loginAction(type: .Dev)
|
||||||
|
|
||||||
|
} label: {
|
||||||
|
makeButton(image: Image(.Logo.logo),
|
||||||
|
color: Color(.Text.black), "계정으로 시작하기")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding([.leading,.trailing], 28)
|
.padding([.leading,.trailing], 28)
|
||||||
|
|
||||||
Spacer(minLength: 1)
|
Spacer(minLength: 1)
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
|
|
||||||
Button {
|
|
||||||
// MARK: TO-DO
|
|
||||||
// 카카오 로그인 연동
|
|
||||||
naviState.set(act: .MOVE, path: .Main)
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 24) {
|
|
||||||
Image("Kakao_Icon")
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 32, height: 32)
|
|
||||||
Text("카카오 계정으로 시작하기")
|
|
||||||
.font(.nps(font: .regular, size: 16))
|
|
||||||
.foregroundStyle(Color(.Text.black))
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(12)
|
|
||||||
.background {
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.foregroundStyle(Color(.Other.yellow))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
/// KAKAO 로그인 버튼
|
|
||||||
|
|
||||||
Button {
|
|
||||||
// MARK: TO-DO
|
|
||||||
// 애플 로그인 연동
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 24) {
|
|
||||||
Image(systemName: "apple.logo")
|
|
||||||
.resizable()
|
|
||||||
.accentColor(Color(.Text.white))
|
|
||||||
.frame(width: 32, height: 32)
|
|
||||||
|
|
||||||
Text("애플 계정으로 시작하기")
|
|
||||||
.font(.nps(font: .regular, size: 16))
|
|
||||||
.foregroundStyle(Color(.Text.white))
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(12)
|
|
||||||
.background {
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.foregroundStyle(Color(.Text.black))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
/// APPLE 로그인 버튼
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
subscribeLoginAction()
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity,maxHeight: .infinity)
|
.frame(maxWidth: .infinity,maxHeight: .infinity)
|
||||||
.fullDrawView(.Normal.normal)
|
.fullDrawView(.Normal.normal)
|
||||||
|
.onAppear() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// .onChange(of: loginVM.pathName){ _, new in
|
||||||
|
// appVM.naviState.set(act: .ADD, path: new)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// .onChange(of: loginVM.toggleLoading) { _, new in
|
||||||
|
// appVM.isLoading = new
|
||||||
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeButton(image: Image, color: Color? = nil, _ body: String) -> some View {
|
func makeButton(image: Image, color: Color? = nil, _ body: String) -> some View {
|
||||||
return HStack {
|
return HStack {
|
||||||
image
|
image
|
||||||
|
@ -131,60 +109,4 @@ struct LoginView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func subscribeLoginAction() {
|
|
||||||
loginVM.loginAction
|
|
||||||
.sink { isTapped in
|
|
||||||
if isTapped {
|
|
||||||
if userId.isEmpty || password.isEmpty {
|
|
||||||
appVM.alertData = SetAlertData().setErrorLogin()
|
|
||||||
appVM.showAlert.toggle()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
}
|
|
||||||
printLog("로그인")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loginAction(type: SNSLoginType) {
|
|
||||||
LoginController().login(type)
|
|
||||||
.flatMap{ snsId in
|
|
||||||
loadAPIData(url: "\(API_URL)",
|
|
||||||
path: "/api/v1/in/user/login",
|
|
||||||
parameters: [
|
|
||||||
"sns_id": "\(snsId.snsId)",
|
|
||||||
"acctype": "\(type == .Apple ? "ST00": "ST01")"
|
|
||||||
],
|
|
||||||
decodingType: APIResponse<User_Academy>.self)
|
|
||||||
|
|
||||||
}
|
|
||||||
.sink { completion in
|
|
||||||
switch completion {
|
|
||||||
case .failure(let error):
|
|
||||||
printLog("\(error)")
|
|
||||||
appVM.isLoading.toggle()
|
|
||||||
case .finished:
|
|
||||||
appVM.isLoading.toggle()
|
|
||||||
}
|
|
||||||
} receiveValue: { response in
|
|
||||||
guard let ua = response as? APIResponse<User_Academy> else {return}
|
|
||||||
if let bids = ua.data.toStringDict()["bid"] {
|
|
||||||
printLog(bids)
|
|
||||||
if let bidArray: [String] = jsonToSwift(bids) {
|
|
||||||
printLog(bidArray[0])
|
|
||||||
} else {
|
|
||||||
printLog("JSON 변환 실패")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//#Preview {
|
|
||||||
// LoginView()
|
|
||||||
//}
|
|
||||||
|
|
293
AcaMate/1. View/11. Intro & Login/RegisterView.swift
Normal file
293
AcaMate/1. View/11. Intro & Login/RegisterView.swift
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
//
|
||||||
|
// RegisterView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/20/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct RegisterView: View {
|
||||||
|
@EnvironmentObject var appVM: AppViewModel
|
||||||
|
@StateObject private var topVM = TopViewModel()
|
||||||
|
@StateObject var btnVM = ButtonViewModel()
|
||||||
|
@StateObject var registerVM: RegisterViewModel
|
||||||
|
|
||||||
|
// private let responseValue: (SNSLoginType, String)
|
||||||
|
|
||||||
|
init(_ appVM: AppViewModel, type: SNSLoginType, snsID: String) {
|
||||||
|
_registerVM = StateObject(wrappedValue: RegisterViewModel(appVM, type: type, snsID: snsID))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@State private var scrollOffset: CGPoint = .zero
|
||||||
|
@State private var showWebView = false
|
||||||
|
@State private var isLoading = false
|
||||||
|
|
||||||
|
@State private var isSelectAddr: Bool = false
|
||||||
|
|
||||||
|
@StateObject private var dropdownManager = DropdownManager()
|
||||||
|
|
||||||
|
|
||||||
|
@State private var onDomainTxf = false
|
||||||
|
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 회원가입 뷰 만들기
|
||||||
|
// 이름, 번호, 이메일, 주소, 생년월일
|
||||||
|
ZStack {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
TopView(topVM: topVM)
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
Text("*")
|
||||||
|
.font(.nps(size: 12))
|
||||||
|
.foregroundStyle(Color(.Other.red))
|
||||||
|
Text("는 필수 입력 사항입니다.")
|
||||||
|
.font(.nps(size: 12))
|
||||||
|
}
|
||||||
|
.padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 16))
|
||||||
|
|
||||||
|
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
|
||||||
|
|
||||||
|
// 이름
|
||||||
|
HStack(spacing: 0){
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("이름")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
Text("*")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
.foregroundStyle(Color(.Other.red))
|
||||||
|
}
|
||||||
|
.frame(width: 60, alignment: .center)
|
||||||
|
.padding(.trailing,12)
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
CustomTxfView(placeholder: "이름 입력 (10 글자)", text: $registerVM.nameText, maxLength: 10, alignment: .center)
|
||||||
|
.frame(maxWidth: .infinity,maxHeight: 48)
|
||||||
|
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.foregroundStyle(Color(.Normal.light))
|
||||||
|
}.clipped()
|
||||||
|
}
|
||||||
|
.padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16))
|
||||||
|
|
||||||
|
// 생년월일
|
||||||
|
HStack(spacing: 0){
|
||||||
|
Text("생일")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
.frame(width: 60, alignment: .center)
|
||||||
|
.padding(.trailing,12)
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
DatePicker("", selection: $registerVM.selectDate, displayedComponents: [.date])
|
||||||
|
.datePickerStyle(.compact)
|
||||||
|
.environment(\.locale, Locale(identifier: "ko_KR"))
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
// E-Mail
|
||||||
|
HStack(spacing: 0){
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("이메일")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
Text("*")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
.foregroundStyle(Color(.Other.red))
|
||||||
|
}
|
||||||
|
.frame(width: 60, alignment: .center)
|
||||||
|
.padding(.trailing,12)
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
|
||||||
|
VStack (spacing: 4) {
|
||||||
|
CustomTxfView(placeholder: "이메일 입력(30 글자)", text: $registerVM.emailFrontText, maxLength: 30, alignment: .center)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.foregroundStyle(Color(.Normal.light))
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("@")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
.padding([.leading, .trailing], 4)
|
||||||
|
// Spacer(minLength: 1)
|
||||||
|
|
||||||
|
DropdownButton(manager: dropdownManager, title: "도메인 선택",
|
||||||
|
items: registerVM.emailTailList){ index in
|
||||||
|
if registerVM.emailTailList.count-1 == index {
|
||||||
|
onDomainTxf = true
|
||||||
|
} else {
|
||||||
|
registerVM.emailTailText = registerVM.emailTailList[index]
|
||||||
|
onDomainTxf = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.foregroundStyle(Color(.Normal.light))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if onDomainTxf {
|
||||||
|
CustomTxfView(placeholder: "도메인 직접 입력(30 글자)", text: $registerVM.emailTailText, maxLength: 30, alignment: .center)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.foregroundStyle(Color(.Normal.light))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
// Phone
|
||||||
|
HStack(spacing: 0){
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("연락처")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
Text("*")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
.foregroundStyle(Color(.Other.red))
|
||||||
|
}
|
||||||
|
.frame(width: 60, alignment: .center)
|
||||||
|
.padding(.trailing,12)
|
||||||
|
DropdownButton(manager: dropdownManager, title: "선택", items: registerVM.numberHeadList){ index in
|
||||||
|
registerVM.phoneTextSet.0 = registerVM.numberHeadList[index]
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.foregroundStyle(Color(.Normal.light))
|
||||||
|
}
|
||||||
|
Text("-")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
.padding([.leading, .trailing], 4)
|
||||||
|
|
||||||
|
CustomTxfView(placeholder: "0000", text: $registerVM.phoneTextSet.1, maxLength: 4, alignment: .center)
|
||||||
|
.frame(maxWidth: .infinity,maxHeight: 48)
|
||||||
|
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.foregroundStyle(Color(.Normal.light))
|
||||||
|
}
|
||||||
|
Text("-")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
.padding([.leading, .trailing], 4)
|
||||||
|
CustomTxfView(placeholder: "0000", text: $registerVM.phoneTextSet.2, maxLength: 4, alignment: .center)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: 48)
|
||||||
|
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.foregroundStyle(Color(.Normal.light))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
HStack(alignment: .center, spacing: 0){
|
||||||
|
Text("주소")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
.frame(width: 60, alignment: .center)
|
||||||
|
.padding(.trailing,12)
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
SimpleBtnView(vm: btnVM, id: registerVM.addressBtnID)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: 48)
|
||||||
|
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.foregroundStyle(Color(.Normal.light))
|
||||||
|
}
|
||||||
|
.padding(.bottom, isSelectAddr ? 4:0)
|
||||||
|
if isSelectAddr {
|
||||||
|
CustomTxfView(placeholder: "상세 주소 입력 (50 글자)", text: $registerVM.addrDetailText, maxLength: 50, alignment: .center)
|
||||||
|
.frame(maxWidth: .infinity,
|
||||||
|
maxHeight: isSelectAddr ? 48 : 0)
|
||||||
|
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.foregroundStyle(Color(.Normal.light))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
|
||||||
|
SimpleBtnView(vm: btnVM, id: registerVM.registerBtnID)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: 48)
|
||||||
|
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
// .background {
|
||||||
|
// RoundedRectangle(cornerRadius: 24)
|
||||||
|
// .foregroundStyle(Color(.Normal.light))
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
GlobalDropdownOverlay(manager: dropdownManager)
|
||||||
|
}
|
||||||
|
.coordinateSpace(name: "dropdownArea")
|
||||||
|
.sheet(isPresented: $showWebView) {
|
||||||
|
WebView(url: URL(string: "https://sean-59.github.io/Kakao-Postcode/")!,
|
||||||
|
showWebView: $showWebView,
|
||||||
|
isLoading: $isLoading) { result in
|
||||||
|
if let result = result as? (String, String) {
|
||||||
|
print(result.0)
|
||||||
|
isSelectAddr = true
|
||||||
|
registerVM.addressText = result.0
|
||||||
|
|
||||||
|
btnVM.setSize(for: registerVM.addressBtnID, newWidth: .infinity, newHeight: .infinity)
|
||||||
|
btnVM.setText(for: registerVM.addressBtnID, newText: "\(registerVM.addressText)", newFont: .nps(size: 16))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
topVM.titleName = "회원가입"
|
||||||
|
|
||||||
|
topVM.setLeftBtn(Image(.Icon.left), size: CGPoint(x: 40, y: 40), action: leftAct)
|
||||||
|
topVM.setRightBtn(size: CGPoint(x: 40, y: 40), action: rightAct)
|
||||||
|
|
||||||
|
btnVM.setSize(for: registerVM.addressBtnID, newWidth: 80, newHeight: 24)
|
||||||
|
btnVM.setText(for: registerVM.addressBtnID, newText: "\(registerVM.addressText)", newFont: .nps(size: 16))
|
||||||
|
btnVM.setTextColor(for: registerVM.addressBtnID, newColor: Color.Text.black)
|
||||||
|
btnVM.setAction(for: registerVM.addressBtnID) { self.showWebView.toggle() }
|
||||||
|
|
||||||
|
btnVM.setSize(for: registerVM.registerBtnID, newWidth: .infinity, newHeight: 48)
|
||||||
|
btnVM.setText(for: registerVM.registerBtnID, newText: "회원가입", newFont: .nps(font: .bold, size: 24))
|
||||||
|
btnVM.setTextColor(for: registerVM.registerBtnID, newColor: Color.Point.dark)
|
||||||
|
btnVM.setAction(for: registerVM.registerBtnID) {
|
||||||
|
Task {
|
||||||
|
await registerVM.registerUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dropdownManager.font = .nps(size: 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func leftAct() {
|
||||||
|
printLog("왼쪽 버튼 클릭")
|
||||||
|
appVM.naviState.set(act: .POP)
|
||||||
|
}
|
||||||
|
func rightAct() {
|
||||||
|
printLog("오른쪽 버튼 클릭")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct selectPolicyView: View {
|
||||||
|
@ObservedObject var registerVM: RegisterViewModel
|
||||||
|
@ObservedObject var btnVM: ButtonViewModel
|
||||||
|
|
||||||
|
init(_ registerVM: RegisterViewModel, btnVM: ButtonViewModel) {
|
||||||
|
self.registerVM = registerVM
|
||||||
|
self.btnVM = btnVM
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
CircleBtnView(vm: btnVM, id: registerVM.policyBtn1ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
152
AcaMate/1. View/11. Intro & Login/SelectAcademyView.swift
Normal file
152
AcaMate/1. View/11. Intro & Login/SelectAcademyView.swift
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
//
|
||||||
|
// SelectAcademyView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/18/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SelectAcademyView: View {
|
||||||
|
@EnvironmentObject var appVM: AppViewModel
|
||||||
|
@StateObject var vm: SelectAcademyViewModel
|
||||||
|
|
||||||
|
init(_ appVM: AppViewModel) {
|
||||||
|
_vm = StateObject(wrappedValue: SelectAcademyViewModel(appVM))
|
||||||
|
}
|
||||||
|
|
||||||
|
@State private var scrollOffset: CGPoint = .zero
|
||||||
|
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
.frame(maxHeight: 100)
|
||||||
|
Image(.Logo.appIcon)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 200, height: 200)
|
||||||
|
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Button {
|
||||||
|
vm.moveChatting()
|
||||||
|
} label: {
|
||||||
|
Text("채팅 진입")
|
||||||
|
}
|
||||||
|
HStack(spacing: 0){
|
||||||
|
Text("학원 코드")
|
||||||
|
.font(.nps(font: .bold, size: 16))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: TO-DO
|
||||||
|
// 문제
|
||||||
|
// 1. txf 클릭시 겉 뷰가 작아지는 현상
|
||||||
|
// 2. 코드 입력시 버튼 나타나게 하기
|
||||||
|
CustomTextField(placeholder: "학원 코드 입력", text: $vm.academyCode)
|
||||||
|
.frame(maxWidth: .infinity,maxHeight: 48)
|
||||||
|
.padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.foregroundStyle(Color(.Normal.light))
|
||||||
|
}
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
.padding(EdgeInsets(top: 12, leading: 24, bottom: 40, trailing: 24))
|
||||||
|
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
HStack(spacing: 0){
|
||||||
|
Text("학원 목록")
|
||||||
|
.font(.nps(font: .bold, size: 16))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
}
|
||||||
|
.padding(EdgeInsets(top: 12, leading: 24, bottom: 0, trailing: 24))
|
||||||
|
if vm.academyList.count > 0 {
|
||||||
|
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
ForEach(Array(vm.academyList.enumerated()), id: \.offset) { index, academy in
|
||||||
|
AcademyCell(numbering: index, academy: vm.academyList[index],selectNum: $vm.selectNum){
|
||||||
|
vm.toggleSelection(for: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(EdgeInsets(top: 0, leading: 24, bottom: 12, trailing: 24))
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
EmptyBoxView(title: "등록된 학원이 없습니다.")
|
||||||
|
.padding(EdgeInsets(top: 0, leading: 24, bottom: 0, trailing: 24))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
appVM.naviState.set(act: .MOVE, path: .Main)
|
||||||
|
} label: {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(Color(.Second.normal))
|
||||||
|
Text("입장하기")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
.foregroundStyle(Color(.Normal.light))
|
||||||
|
}
|
||||||
|
.frame(height: 56)
|
||||||
|
}
|
||||||
|
.opacity(vm.selectNum >= 0 ? 1 : 0)
|
||||||
|
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||||
|
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
vm.loadAcademy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AcademyCell: View {
|
||||||
|
let numbering: Int
|
||||||
|
let academy: AcademyName
|
||||||
|
@Binding var selectNum: Int
|
||||||
|
let action: VOID_TO_VOID
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .center, spacing: 0) {
|
||||||
|
Image(.Logo.pageIcon)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 32, height: 32, alignment: .center)
|
||||||
|
.padding(12)
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
|
||||||
|
Text("\(academy.name)")
|
||||||
|
.font(.nps(size: 18))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
.multilineStyle(.center)
|
||||||
|
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
|
||||||
|
Button{
|
||||||
|
// saVM.toggleSelection(for: numbering)
|
||||||
|
action()
|
||||||
|
} label: {
|
||||||
|
Circle()
|
||||||
|
.stroke(Color(.Text.detail), lineWidth: 4)
|
||||||
|
.fill (selectNum == numbering ? Color(.Point.normal) : Color(.Normal.normal))
|
||||||
|
.frame(width: 18, height: 18)
|
||||||
|
}
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.padding(12)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.stroke(Color(.Second.normal), lineWidth: 2)
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,20 +50,21 @@ struct AttCellView: View {
|
||||||
.padding(.top,4)
|
.padding(.top,4)
|
||||||
HStack(alignment: .center, spacing: 2) {
|
HStack(alignment: .center, spacing: 2) {
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("\(valueGroup.0)").font(.nps(font: .bold, size: 20))
|
Text("\(valueGroup.0)")
|
||||||
|
.font(.nps(font: .bold, size: 20))
|
||||||
.foregroundStyle(((Double(valueGroup.0)/Double(valueGroup.1)) < 0.7) ? Color(.Other.red) : Color(.Other.blue))
|
.foregroundStyle(((Double(valueGroup.0)/Double(valueGroup.1)) < 0.7) ? Color(.Other.red) : Color(.Other.blue))
|
||||||
.frame(width: 28,alignment: .center)
|
.frame(width: 28,alignment: .center)
|
||||||
Text("/").font(.nps(size: 12))
|
Text("/")
|
||||||
|
.font(.nps(size: 12))
|
||||||
.foregroundStyle(Color(.Text.detail))
|
.foregroundStyle(Color(.Text.detail))
|
||||||
Text("\(valueGroup.1)").font(.nps(font: .bold, size: 20))
|
Text("\(valueGroup.1)")
|
||||||
|
.font(.nps(font: .bold, size: 20))
|
||||||
.foregroundStyle(Color(.Text.detail))
|
.foregroundStyle(Color(.Text.detail))
|
||||||
.frame(width: 28,alignment: .center)
|
.frame(width: 28,alignment: .center)
|
||||||
Text("\(cellText.group)")
|
Text("\(cellText.group)")
|
||||||
.font(.nps(size: 16))
|
.font(.nps(size: 16))
|
||||||
.foregroundStyle(Color(.Text.detail))
|
.foregroundStyle(Color(.Text.detail))
|
||||||
.lineLimit(1)
|
.multilineStyle()
|
||||||
.minimumScaleFactor(0.5)
|
|
||||||
.truncationMode(.tail)
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,9 +48,7 @@ struct CalCellView: View {
|
||||||
Text("\(summaryCalData.summary)")
|
Text("\(summaryCalData.summary)")
|
||||||
.font(.nps(size: 20))
|
.font(.nps(size: 20))
|
||||||
.foregroundStyle(Color(.Text.black))
|
.foregroundStyle(Color(.Text.black))
|
||||||
.lineLimit(1)
|
.multilineStyle()
|
||||||
.minimumScaleFactor(0.5)
|
|
||||||
.truncationMode(.tail)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,26 +8,39 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct HomeView: View {
|
struct HomeView: View {
|
||||||
|
@StateObject private var topVM = TopViewModel()
|
||||||
|
|
||||||
@State private var scrollOffset: CGPoint = .zero
|
@State private var scrollOffset: CGPoint = .zero
|
||||||
@State private var topViewState: Bool = false
|
@State private var topViewState: Bool = false
|
||||||
|
|
||||||
|
//MARK: - 변경 값
|
||||||
|
@Binding var myType: UserType
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
if !topViewState {
|
||||||
|
VStack {
|
||||||
|
Rectangle()
|
||||||
|
.foregroundStyle(Color(.Other.cell))
|
||||||
|
.frame(height: 100 + (scrollOffset.y < 0 ? scrollOffset.y * -1 : 0))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
|
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
|
||||||
VStack(spacing: 0) {
|
LazyVStack(spacing: 24) {
|
||||||
|
|
||||||
TopProfileView(userType: .Student)
|
TopProfileView(myType: myType)
|
||||||
.padding(.bottom,12)
|
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
AttendanceBoxView()
|
AttendanceBoxView()
|
||||||
|
|
||||||
// CalendarBoxView(summaryCalDataList: [])
|
|
||||||
CalendarBoxView(summaryCalDataList: [
|
CalendarBoxView(summaryCalDataList: [
|
||||||
SummaryCalendar(id: "123", date: "2025-02-28", summary: "요약내용입니다."),
|
SummaryCalendar(id: "123", date: "2025-02-28", summary: "요약내용입니다."),
|
||||||
SummaryCalendar(id: "123", date: "2025-02-28", summary: "요약내용입니다.")])
|
SummaryCalendar(id: "123", date: "2025-02-28", summary: "요약내용입니다.")])
|
||||||
|
|
||||||
// ManagementBoxView(managementList: [])
|
|
||||||
ManagementBoxView(managementList: [
|
ManagementBoxView(managementList: [
|
||||||
SummaryManagement(id: "01", title: "과목 명1", teacher: "A", ratio: 27, homework: 3),
|
SummaryManagement(id: "01", title: "과목 명1", teacher: "A", ratio: 27, homework: 3),
|
||||||
SummaryManagement(id: "02", title: "과목 명2", teacher: "B", ratio: 80, homework: 10),
|
SummaryManagement(id: "02", title: "과목 명2", teacher: "B", ratio: 80, homework: 10),
|
||||||
|
@ -35,7 +48,6 @@ struct HomeView: View {
|
||||||
SummaryManagement(id: "04", title: "과목 명4", teacher: "D", ratio: 72, homework: 0),
|
SummaryManagement(id: "04", title: "과목 명4", teacher: "D", ratio: 72, homework: 0),
|
||||||
])
|
])
|
||||||
|
|
||||||
// NoticeBoxView(noticeList: [])
|
|
||||||
NoticeBoxView(noticeList: [
|
NoticeBoxView(noticeList: [
|
||||||
SummaryNotice(id: "00", title: "공지사항1", date: "2025-02-11", new: true),
|
SummaryNotice(id: "00", title: "공지사항1", date: "2025-02-11", new: true),
|
||||||
SummaryNotice(id: "01", title: "공지사항2", date: "2025-01-11", new: false),
|
SummaryNotice(id: "01", title: "공지사항2", date: "2025-01-11", new: false),
|
||||||
|
@ -56,14 +68,13 @@ struct HomeView: View {
|
||||||
RoundedRectangle(cornerRadius: 8)
|
RoundedRectangle(cornerRadius: 8)
|
||||||
.foregroundStyle(Color(.Other.cell))
|
.foregroundStyle(Color(.Other.cell))
|
||||||
}
|
}
|
||||||
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
.padding([.leading, .trailing], 24)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if topViewState {
|
if topViewState {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
TopView(titleName: "Name")
|
TopView(topVM: topVM)
|
||||||
.transition(.move(edge: .top))
|
.transition(.move(edge: .top))
|
||||||
.animation(.easeInOut, value: scrollOffset)
|
.animation(.easeInOut, value: scrollOffset)
|
||||||
Spacer(minLength: 1)
|
Spacer(minLength: 1)
|
||||||
|
@ -72,6 +83,20 @@ struct HomeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.onAppear {
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 여기도 수정봐야 함
|
||||||
|
topVM.titleName = "Name"
|
||||||
|
|
||||||
|
if myType == .Student {
|
||||||
|
topVM.setLeftBtn(Image(.Icon.face), size: CGPoint(x: 40, y: 40), action: leftAct)
|
||||||
|
} else {
|
||||||
|
topVM.setLeftBtn(text: "\(myType.rawValue)", font: .nps(font: .bold, size: 24),
|
||||||
|
size: CGPoint(x: 40, y: 40), action: leftAct)
|
||||||
|
}
|
||||||
|
topVM.setRightBtn(Image(.Icon.notificationSET), size: CGPoint(x: 40, y: 40), action: rightAct)
|
||||||
|
topVM.btnVM.setImage(for: topVM.rightBtnID, newImage: Image(.Icon.notificationSET))
|
||||||
|
}
|
||||||
.onChange(of: scrollOffset.y) { oldValue, newValue in
|
.onChange(of: scrollOffset.y) { oldValue, newValue in
|
||||||
if newValue > 200 && topViewState == false {
|
if newValue > 200 && topViewState == false {
|
||||||
topViewState = true
|
topViewState = true
|
||||||
|
@ -80,23 +105,13 @@ struct HomeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func leftAct() {
|
||||||
|
printLog("왼쪽 버튼 클릭")
|
||||||
|
}
|
||||||
|
func rightAct() {
|
||||||
|
printLog("오른쪽 버튼 클릭")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EmptyBoxView: View {
|
|
||||||
let title: String
|
|
||||||
var body: some View {
|
|
||||||
Text("\(title)")
|
|
||||||
.font(.nps(size: 20))
|
|
||||||
.foregroundStyle(Color(.Text.detail))
|
|
||||||
.lineLimit(1)
|
|
||||||
.minimumScaleFactor(0.5)
|
|
||||||
.truncationMode(.tail)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding([.top,.bottom],12)
|
|
||||||
.background {
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.stroke(Color(.Second.normal), lineWidth: 2)
|
|
||||||
.fill(Color(.Second.light))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
||||||
struct TopProfileView: View {
|
struct TopProfileView: View {
|
||||||
@StateObject var btnVM = ButtonViewModel()
|
@StateObject var btnVM = ButtonViewModel()
|
||||||
|
|
||||||
var userType: UserType
|
var myType: UserType
|
||||||
|
|
||||||
// MARK: TO-DO
|
// MARK: TO-DO
|
||||||
// 여기서 이름 떙겨오는것도 고민을 해야 함
|
// 여기서 이름 떙겨오는것도 고민을 해야 함
|
||||||
|
@ -48,27 +48,24 @@ struct TopProfileView: View {
|
||||||
.padding([.top, .bottom], 40)
|
.padding([.top, .bottom], 40)
|
||||||
VStack(alignment: .center, spacing: 8) {
|
VStack(alignment: .center, spacing: 8) {
|
||||||
Text("\(self.academyName)")
|
Text("\(self.academyName)")
|
||||||
.frame(alignment: .center)
|
|
||||||
.font(.nps(font: .bold, size: 36))
|
.font(.nps(font: .bold, size: 36))
|
||||||
.foregroundStyle(Color(.Text.title))
|
.foregroundStyle(Color(.Text.title))
|
||||||
.lineLimit(1)
|
.multilineStyle()
|
||||||
.minimumScaleFactor(0.5)
|
|
||||||
.truncationMode(.tail)
|
|
||||||
Text("\(self.myName)")
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.frame(alignment: .center)
|
.frame(alignment: .center)
|
||||||
|
|
||||||
|
Text("\(self.myName)")
|
||||||
.font(.nps(size: 18))
|
.font(.nps(size: 18))
|
||||||
.foregroundStyle(Color(.Text.detail))
|
.foregroundStyle(Color(.Text.detail))
|
||||||
.lineLimit(1)
|
.multilineStyle()
|
||||||
.minimumScaleFactor(0.5)
|
.frame(alignment: .center)
|
||||||
.truncationMode(.tail)
|
|
||||||
}
|
}
|
||||||
} /// 위쪽 VStack
|
} /// 위쪽 VStack
|
||||||
.padding(EdgeInsets(top: 24, leading: 24, bottom: 12, trailing: 24))
|
.padding(EdgeInsets(top: 24, leading: 24, bottom: 12, trailing: 24))
|
||||||
|
|
||||||
// MARK: TO-DO
|
// MARK: TO-DO
|
||||||
// 여기에 가로스크롤 넣어야 할거 같음
|
// 여기에 가로스크롤 넣어야 할거 같음
|
||||||
if userType == .Parent {
|
if myType == .Parent {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
ForEach(Array(childIDList.enumerated()),id: \.offset){ index, id in
|
ForEach(Array(childIDList.enumerated()),id: \.offset){ index, id in
|
||||||
CircleBtnView(vm: btnVM, id: id)
|
CircleBtnView(vm: btnVM, id: id)
|
||||||
|
@ -95,7 +92,7 @@ struct TopProfileView: View {
|
||||||
// 마켓 버튼과 알림 버튼 동작 로직 구현하기
|
// 마켓 버튼과 알림 버튼 동작 로직 구현하기
|
||||||
|
|
||||||
|
|
||||||
switch self.userType {
|
switch self.myType {
|
||||||
case .Student:
|
case .Student:
|
||||||
typeName = "학생"
|
typeName = "학생"
|
||||||
case .Parent:
|
case .Parent:
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
//
|
|
||||||
// ManagementView.swift
|
|
||||||
// AcaMate
|
|
||||||
//
|
|
||||||
// Created by TAnine on 2/11/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ManagementView: View {
|
|
||||||
var body: some View {
|
|
||||||
Text("학습 관리")
|
|
||||||
}
|
|
||||||
}
|
|
91
AcaMate/1. View/12. Main/123. Chatting/ChatListView.swift
Normal file
91
AcaMate/1. View/12. Main/123. Chatting/ChatListView.swift
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
//
|
||||||
|
// ChatListView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/14/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ChatListView: View {
|
||||||
|
var chatList: [SummaryChat]
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
ForEach(Array(chatList.enumerated()), id:\.offset) { index, chat in
|
||||||
|
ChatCellView(summaryChat: chat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatCellView: View {
|
||||||
|
@EnvironmentObject var appVM: AppViewModel
|
||||||
|
|
||||||
|
var summaryChat: SummaryChat
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
HStack(alignment: .bottom, spacing: 4) {
|
||||||
|
Image(.Icon.chatting).resizable()
|
||||||
|
.frame(width: 24, height: 24, alignment: .center)
|
||||||
|
|
||||||
|
Text("\(summaryChat.chatName)")
|
||||||
|
.font(.nps(size: 20))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
.multilineStyle()
|
||||||
|
|
||||||
|
if summaryChat.groupNum > 0 {
|
||||||
|
HStack(alignment: .center, spacing: 0) {
|
||||||
|
Image(.Icon.person).resizable()
|
||||||
|
.frame(width: 12, height: 12, alignment: .center)
|
||||||
|
Text("\(summaryChat.groupNum)")
|
||||||
|
.font(.nps(size: 8))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
|
||||||
|
HStack(alignment: .bottom, spacing: 4) {
|
||||||
|
Image(summaryChat.notiState ? .Icon.notificationON : .Icon.notificationOFF).resizable()
|
||||||
|
.frame(width: 12, height: 12, alignment: .center)
|
||||||
|
Text("\(summaryChat.teacherName)")
|
||||||
|
.font(.nps(font: .bold, size: 12))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
Text("선생님")
|
||||||
|
.font(.nps(size: 8))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Text("\(summaryChat.lastMessage)")
|
||||||
|
.font(.nps(size: 10))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
.multilineStyle(limit: 2, scale: 1)
|
||||||
|
.padding(.trailing, 8)
|
||||||
|
|
||||||
|
HStack(alignment: .bottom, spacing: 4) {
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
Group {
|
||||||
|
Text("\(summaryChat.dayDate)")
|
||||||
|
.font(.nps(size: 8))
|
||||||
|
Text("\(summaryChat.timeDate)")
|
||||||
|
.font(.nps(font: .bold, size: 12))
|
||||||
|
}
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12))
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.stroke(Color(.Second.normal), lineWidth: 2)
|
||||||
|
.fill(Color(.Second.light))
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
printLog("채팅 내부 셀 클릭")
|
||||||
|
// MARK: TO-DO
|
||||||
|
appVM.naviState.set(act: .ADD, path: .ChatRoom(id: summaryChat.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
//
|
||||||
|
// ChattingRoomView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/17/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ChattingRoomView: View {
|
||||||
|
@EnvironmentObject var appVM: AppViewModel
|
||||||
|
@StateObject private var topVM = TopViewModel()
|
||||||
|
|
||||||
|
|
||||||
|
// @StateObject private var chatVM = ChatViewModel()
|
||||||
|
|
||||||
|
@StateObject private var btnVM = ButtonViewModel()
|
||||||
|
@State var sendBtnID = UUID()
|
||||||
|
@State var addSomeBtnID = UUID()
|
||||||
|
@State var callCameraBtnID = UUID()
|
||||||
|
@State var addPhotoBtnID = UUID()
|
||||||
|
|
||||||
|
@State private var scrollOffset: CGPoint = .zero
|
||||||
|
|
||||||
|
@StateObject private var socketManager = WebSocketManager()
|
||||||
|
@State private var message = ""
|
||||||
|
|
||||||
|
let roomID: String
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .center, spacing: 0) {
|
||||||
|
TopView(topVM: topVM)
|
||||||
|
Text("Hello, World! \(roomID)")
|
||||||
|
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
|
||||||
|
|
||||||
|
}.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
|
||||||
|
TextField("SEND MESSAGE", text: $message)
|
||||||
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
|
.padding()
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Button {
|
||||||
|
socketManager.connect()
|
||||||
|
} label: {
|
||||||
|
Text("CONNECT")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
socketManager.sendMessage(message)
|
||||||
|
message = ""
|
||||||
|
} label: {
|
||||||
|
Text("SEND")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
socketManager.disconnect()
|
||||||
|
} label: {
|
||||||
|
Text("DISCONNECT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List(socketManager.receivedMessage, id: \.self) { msg in
|
||||||
|
Text(msg)
|
||||||
|
.foregroundStyle(Color(.Other.cell))
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullDrawView(Color(.Other.cell))
|
||||||
|
}
|
||||||
|
|
||||||
|
.onAppear {
|
||||||
|
topVM.titleName = "클래스 명"
|
||||||
|
topVM.setLeftBtn(Image(.Icon.back), size: CGPoint(x:40, y:40)) {
|
||||||
|
appVM.naviState.set(act: .POP, path: .ChatRoom(id: roomID))
|
||||||
|
}
|
||||||
|
topVM.setRightBtn(Image(.Icon.setting), size: CGPoint(x: 40, y: 40)) {
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 채팅방 설정 열기
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,183 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ChattingView: View {
|
struct ChattingView: View {
|
||||||
|
@StateObject private var topVM = TopViewModel()
|
||||||
|
@StateObject private var btnVM = ButtonViewModel()
|
||||||
|
@StateObject private var vm: ChatViewModel
|
||||||
|
|
||||||
|
@State private var scrollOffset: CGPoint = .zero
|
||||||
|
|
||||||
|
@State private var leftBtnID = UUID()
|
||||||
|
@State private var rightBtnID = UUID()
|
||||||
|
|
||||||
|
|
||||||
|
init(_ appVM: AppViewModel, _ myType: Binding<UserType>) {
|
||||||
|
_vm = StateObject(wrappedValue: ChatViewModel(appVM))
|
||||||
|
_myType = myType
|
||||||
|
}
|
||||||
|
|
||||||
|
let classList = [
|
||||||
|
SummaryChat(id: "00", chatName: "Class 101", teacherName: "홍길동",
|
||||||
|
lastMessage: "여기에는 채팅이 나올 예정입니다. 2줄 정도로 나올 예정이며 끝자리는 잘려서 나올 것 입니다. 이정도의 채팅으로는 택도 없어서 조금 더 길게 길게 작성을 해봅니다.",
|
||||||
|
dayDate: "2025. 02. 14", timeDate: "PM 11:00", notiState: true, groupNum: 12),
|
||||||
|
SummaryChat(id: "01", chatName: "Class 101", teacherName: "홍길동",
|
||||||
|
lastMessage: "여기에는 채팅이 나올 예정입니다. 2줄 정도로 나올 예정이며 끝자리는 잘려서 나올 것 입니다. 이정도의 채팅으로는 택도 없어서 조금 더 길게 길게 작성을 해봅니다.",
|
||||||
|
dayDate: "2025. 02. 14", timeDate: "PM 11:00", notiState: false, groupNum: 12),
|
||||||
|
SummaryChat(id: "02", chatName: "Class 101", teacherName: "홍길동",
|
||||||
|
lastMessage: "여기에는 채팅이 나올 예정입니다. 2줄 정도로 나올 예정이며 끝자리는 잘려서 나올 것 입니다. 이정도의 채팅으로는 택도 없어서 조금 더 길게 길게 작성을 해봅니다.",
|
||||||
|
dayDate: "2025. 02. 14", timeDate: "PM 11:00", notiState: true, groupNum: 12)
|
||||||
|
]
|
||||||
|
|
||||||
|
//MARK: - 변경 값
|
||||||
|
@Binding var myType: UserType
|
||||||
|
|
||||||
|
@State var chatMenu: chatType = .Class
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text("채팅")
|
VStack(spacing: 0) {
|
||||||
|
TopView(topVM: topVM)
|
||||||
|
if myType == .ETC || myType == .Employee {
|
||||||
|
EmptyBoxView(title: "이용하실 수 없는 기능입니다.")
|
||||||
|
.padding(24)
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if myType == .Teacher || myType == .Admin {
|
||||||
|
if myType == .Admin {
|
||||||
|
HStack {
|
||||||
|
SimpleBtnView(vm: btnVM, id: leftBtnID)
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
Text("선생님 이름")
|
||||||
|
.font(.nps(font: .bold, size: 24))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
SimpleBtnView(vm: btnVM, id: rightBtnID)
|
||||||
|
}
|
||||||
|
.padding(EdgeInsets(top: 12, leading: 24, bottom: 0, trailing: 24))
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
SelectChatMenu(chatMenu: $chatMenu, tag: .Class, image: Image(.Icon.group), title: "클래스")
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
SelectChatMenu(chatMenu: $chatMenu, tag: .Student, image: Image(.Icon.talk), title: "학생")
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
SelectChatMenu(chatMenu: $chatMenu, tag: .Parent, image: Image(.Icon.talk), title: "학부모")
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||||
|
}
|
||||||
|
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
|
||||||
|
LazyVStack(spacing: 24) {
|
||||||
|
if myType == .Student || myType == .Parent {
|
||||||
|
Group {
|
||||||
|
DashBoardView(image: Image(.Icon.group), title: "클래스") {
|
||||||
|
ChatListView(chatList: classList)
|
||||||
|
}
|
||||||
|
|
||||||
|
DashBoardView(image: Image(.Icon.talk), title: "선생님과 1:1") {
|
||||||
|
|
||||||
|
}
|
||||||
|
DashBoardView(image: Image(.Icon.talk), title: "부모님과 1:1") {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.foregroundStyle(Color(.Other.cell))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Group {
|
||||||
|
switch chatMenu {
|
||||||
|
case .Class:
|
||||||
|
DashBoardView(image: Image(.Icon.group), title: "클래스") {
|
||||||
|
ChatListView(chatList: classList)
|
||||||
|
}
|
||||||
|
case .Student:
|
||||||
|
DashBoardView(image: Image(.Icon.talk), title: "학생과 1:1") {
|
||||||
|
|
||||||
|
}
|
||||||
|
case .Parent:
|
||||||
|
DashBoardView(image: Image(.Icon.talk), title: "부모님과 1:1") {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.foregroundStyle(Color(.Other.cell))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(EdgeInsets(
|
||||||
|
top: (myType == .Student || myType == .Parent) ? 24 : 12, leading: 24, bottom: 24, trailing: 24))
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 여기도 수정봐야 함
|
||||||
|
topVM.titleName = "Name"
|
||||||
|
|
||||||
|
if myType == .Student {
|
||||||
|
topVM.setLeftBtn(Image(.Icon.face), size: CGPoint(x: 40, y: 40), action: leftAct)
|
||||||
|
} else {
|
||||||
|
topVM.setLeftBtn(text: "\(myType.rawValue)", font: .nps(font: .bold, size: 24),
|
||||||
|
size: CGPoint(x: 40, y: 40), action: leftAct)
|
||||||
|
}
|
||||||
|
topVM.setRightBtn(Image(.Icon.plus), size: CGPoint(x: 40, y: 40), action: rightAct)
|
||||||
|
|
||||||
|
btnVM.setImage(for: leftBtnID, newImage: Image(.Icon.left))
|
||||||
|
btnVM.setImage(for: rightBtnID, newImage: Image(.Icon.right))
|
||||||
|
|
||||||
|
btnVM.setSize(for: leftBtnID, newWidth: 24, newHeight: 24)
|
||||||
|
btnVM.setSize(for: rightBtnID, newWidth: 24, newHeight: 24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func leftAct() {
|
||||||
|
printLog("왼쪽 버튼 클릭")
|
||||||
|
}
|
||||||
|
func rightAct() {
|
||||||
|
printLog("오른쪽 버튼 클릭")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SelectChatMenu: View {
|
||||||
|
@Binding var chatMenu: chatType
|
||||||
|
let tag: chatType
|
||||||
|
let image: Image
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .center, spacing: 4) {
|
||||||
|
if chatMenu == tag {
|
||||||
|
image.resizable()
|
||||||
|
.frame(width: 24, height: 24, alignment: .center)
|
||||||
|
Text("\(title)")
|
||||||
|
.font(.nps(font: .bold, size: 24))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
} else {
|
||||||
|
image.resizable()
|
||||||
|
.renderingMode(.template)
|
||||||
|
.frame(width: 24, height: 24, alignment: .center)
|
||||||
|
.foregroundStyle(Color(.Disable.normal))
|
||||||
|
Text("\(title)")
|
||||||
|
.font(.nps(size: 20))
|
||||||
|
.foregroundStyle(Color(.Disable.normal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(2)
|
||||||
|
.background {
|
||||||
|
if chatMenu == tag {
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.foregroundStyle(Color.Other.cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
chatMenu = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
40
AcaMate/1. View/12. Main/123. Chatting/ManagementView.swift
Normal file
40
AcaMate/1. View/12. Main/123. Chatting/ManagementView.swift
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
//
|
||||||
|
// ManagementView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/11/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ManagementView: View {
|
||||||
|
@StateObject private var topVM = TopViewModel()
|
||||||
|
|
||||||
|
@State private var scrollOffset: CGPoint = .zero
|
||||||
|
var body: some View {
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
TopView(topVM: topVM)
|
||||||
|
|
||||||
|
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
|
||||||
|
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
topVM.titleName = ""
|
||||||
|
topVM.setLeftBtn(size: CGPoint(x: 40, y: 40), action: leftAct)
|
||||||
|
topVM.setRightBtn(size: CGPoint(x: 40, y: 40), action: rightAct)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func leftAct() {
|
||||||
|
printLog("왼쪽 버튼 클릭")
|
||||||
|
}
|
||||||
|
func rightAct() {
|
||||||
|
printLog("오른쪽 버튼 클릭")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,33 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct CalendarView: View {
|
struct CalendarView: View {
|
||||||
|
@StateObject private var topVM = TopViewModel()
|
||||||
|
|
||||||
|
@State private var scrollOffset: CGPoint = .zero
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text("일정")
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
TopView(topVM: topVM)
|
||||||
|
|
||||||
|
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
|
||||||
|
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
topVM.titleName = ""
|
||||||
|
topVM.setLeftBtn(size: CGPoint(x: 40, y: 40), action: leftAct)
|
||||||
|
topVM.setRightBtn(size: CGPoint(x: 40, y: 40), action: rightAct)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func leftAct() {
|
||||||
|
printLog("왼쪽 버튼 클릭")
|
||||||
|
}
|
||||||
|
func rightAct() {
|
||||||
|
printLog("오른쪽 버튼 클릭")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
34
AcaMate/1. View/12. Main/125. Etc/AppInfoView.swift
Normal file
34
AcaMate/1. View/12. Main/125. Etc/AppInfoView.swift
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
//
|
||||||
|
// AppInfoView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/13/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AppInfoView: View {
|
||||||
|
@UserDefault(key:"currentVer", defaultValue: "0.0.0") var currentVer
|
||||||
|
@UserDefault(key:"finalVer", defaultValue: "0.0.0") var finalVer
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
EtcBoxView(title: "앱 정보"){
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Image(.Logo.pageIcon)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 40, height: 40, alignment: .center)
|
||||||
|
.padding(.trailing, 12)
|
||||||
|
Text("설치 버전: \(currentVer)")
|
||||||
|
.font(.nps(font: .bold, size: 16))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
if currentVer == finalVer {
|
||||||
|
Text("최신버전입니다.")
|
||||||
|
.font(.nps(size: 12))
|
||||||
|
.foregroundStyle(Color(.Normal.light))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding([.top,.bottom],12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
AcaMate/1. View/12. Main/125. Etc/CsCenterView.swift
Normal file
45
AcaMate/1. View/12. Main/125. Etc/CsCenterView.swift
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
//
|
||||||
|
// CsCenterView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/13/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CsCenterView: View {
|
||||||
|
var body: some View {
|
||||||
|
EtcBoxView(title: "고객 센터") {
|
||||||
|
VStack(spacing: 0){
|
||||||
|
EtcCellView(title: "공지사항") {
|
||||||
|
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 공지사항 이동
|
||||||
|
printLog("공지사항 이동")
|
||||||
|
}
|
||||||
|
DashedDivider()
|
||||||
|
EtcCellView(title: "1:1 문의") {
|
||||||
|
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 1:1 문의 이동
|
||||||
|
printLog("1:1 문의 이동")
|
||||||
|
}
|
||||||
|
DashedDivider()
|
||||||
|
EtcCellView(title: "자주 묻는 질문") {
|
||||||
|
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 자주 묻는 질문 이동
|
||||||
|
printLog("자주 묻는 질문 이동")
|
||||||
|
}
|
||||||
|
DashedDivider()
|
||||||
|
EtcCellView(title: "학원 정보") {
|
||||||
|
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 학원 정보 이동
|
||||||
|
printLog("학원 정보 이동")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
67
AcaMate/1. View/12. Main/125. Etc/DevInfoView.swift
Normal file
67
AcaMate/1. View/12. Main/125. Etc/DevInfoView.swift
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
//
|
||||||
|
// DevInfoView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/13/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct DevInfoView: View {
|
||||||
|
|
||||||
|
private var attributeText: AttributedString {
|
||||||
|
let longText = """
|
||||||
|
아래 정보는 해당 프로그램에 관한 정보로서
|
||||||
|
학원에 대한 문의는 더보기 > 고객센터 > 1:1 문의를 이용해주시기 바랍니다.
|
||||||
|
"""
|
||||||
|
var attributeText = AttributedString(longText)
|
||||||
|
attributeText.font = .nps(size: 8)
|
||||||
|
if let range = attributeText.range(of: "더보기 > 고객센터 > 1:1 문의") {
|
||||||
|
attributeText[range].font = .nps(font: .bold, size: 8)
|
||||||
|
}
|
||||||
|
return attributeText
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 24) {
|
||||||
|
|
||||||
|
Text(attributeText)
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
.multilineStyle(.center, limit: 2)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 12){
|
||||||
|
HStack(alignment: .center, spacing: 8) {
|
||||||
|
Image(.Logo.appIcon).resizable()
|
||||||
|
.frame(width: 40, height: 40,alignment: .leading)
|
||||||
|
Text("AcaMate")
|
||||||
|
.font(.nps(font: .bold, size: 16))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
}
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 이부분에 대한 내용은 후에 바꿀 예정
|
||||||
|
Text(verbatim:"""
|
||||||
|
문의: sean.kk@daum.net
|
||||||
|
문의: sean.kk@daum.net
|
||||||
|
""")
|
||||||
|
.font(.nps(size: 12))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("Copyright © Team.Stein. All Rights Reserved")
|
||||||
|
.font(.nps(size: 14))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
|
||||||
|
// .padding([.leading,.trailing],24)
|
||||||
|
}
|
||||||
|
.padding(24)
|
||||||
|
// .padding([.top,.bottom],24)
|
||||||
|
.background {
|
||||||
|
Color(.Disable.normal)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
63
AcaMate/1. View/12. Main/125. Etc/EtcBoxView.swift
Normal file
63
AcaMate/1. View/12. Main/125. Etc/EtcBoxView.swift
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
//
|
||||||
|
// EtcBoxView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/13/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EtcBoxView<Content: View>: View {
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
@ViewBuilder let content: Content
|
||||||
|
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 24) {
|
||||||
|
Text("\(title)")
|
||||||
|
.font(.nps(font: .bold, size: 24))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
content
|
||||||
|
.padding([.leading,.trailing],24)
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.foregroundStyle(Color(.Other.cell))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct EtcCellView: View {
|
||||||
|
@StateObject var btnVM = ButtonViewModel()
|
||||||
|
let nextBtnID = UUID()
|
||||||
|
let title: String
|
||||||
|
let action: VOID_TO_VOID
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("\(title)")
|
||||||
|
.font(.nps(font: .bold, size: 20))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
SimpleBtnView(vm: btnVM, id: nextBtnID)
|
||||||
|
.padding([.top,.bottom],24)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
btnVM.setSize(for: nextBtnID, newWidth: 24, newHeight: 24)
|
||||||
|
btnVM.setImage(for: nextBtnID, newImage: Image(.Icon.right))
|
||||||
|
btnVM.setAction(for: nextBtnID, newAction: action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DashedDivider: View {
|
||||||
|
var body: some View {
|
||||||
|
Rectangle()
|
||||||
|
.stroke(style: StrokeStyle(lineWidth: 1, dash: [5, 3]))
|
||||||
|
.foregroundColor(Color.Disable.normal)
|
||||||
|
.frame(height: 1)
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,8 +8,46 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct EtcView: View {
|
struct EtcView: View {
|
||||||
|
@StateObject private var topVM = TopViewModel()
|
||||||
|
@State private var scrollOffset: CGPoint = .zero
|
||||||
|
|
||||||
|
@Binding var myType: UserType
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text("더보기")
|
VStack(spacing: 0) {
|
||||||
|
TopView(topVM: topVM)
|
||||||
|
|
||||||
|
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
|
||||||
|
VStack(spacing: 24) {
|
||||||
|
UserInfoView(userData: SummaryUser(profile: Image(.Icon.face), name: "이름", userID: "abcdefg", email: "abcdefg@gmail.com"))
|
||||||
|
UserSettingView(myType: myType)
|
||||||
|
CsCenterView()
|
||||||
|
TsCsView()
|
||||||
|
AppInfoView()
|
||||||
|
}
|
||||||
|
.padding(24)
|
||||||
|
DevInfoView()
|
||||||
|
|
||||||
|
// Rectangle()
|
||||||
|
// .foregroundStyle(Color(.Disable.normal))
|
||||||
|
// .frame(height: 500)
|
||||||
|
// .frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
topVM.titleName = ""
|
||||||
|
topVM.setLeftBtn(size: CGPoint(x: 40, y: 40), action: leftAct)
|
||||||
|
topVM.setRightBtn(size: CGPoint(x: 40, y: 40), action: rightAct)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func leftAct() {
|
||||||
|
printLog("왼쪽 버튼 클릭")
|
||||||
|
}
|
||||||
|
func rightAct() {
|
||||||
|
printLog("오른쪽 버튼 클릭")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
29
AcaMate/1. View/12. Main/125. Etc/TsCsView.swift
Normal file
29
AcaMate/1. View/12. Main/125. Etc/TsCsView.swift
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
//
|
||||||
|
// TsCsView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/13/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TsCsView: View {
|
||||||
|
var body: some View {
|
||||||
|
EtcBoxView(title: "약관"){
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
EtcCellView(title: "이용약관") {
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 이용약관 이동
|
||||||
|
printLog("이용약관 이동")
|
||||||
|
}
|
||||||
|
DashedDivider()
|
||||||
|
EtcCellView(title: "개인정보 처리방침") {
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 개인정보 처리방침 이동
|
||||||
|
printLog("개인정보 처리방침 이동")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 이용약관 개인정보 처리방침
|
71
AcaMate/1. View/12. Main/125. Etc/UserInfoView.swift
Normal file
71
AcaMate/1. View/12. Main/125. Etc/UserInfoView.swift
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
//
|
||||||
|
// UserInfoView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/13/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct UserInfoView: View {
|
||||||
|
@StateObject var btnVM = ButtonViewModel()
|
||||||
|
|
||||||
|
@State var notifyBtnID = UUID()
|
||||||
|
@State var userData: SummaryUser
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 8 ) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
userData.profile.resizable()
|
||||||
|
.frame(width: 30, height: 30, alignment: .center)
|
||||||
|
Text("\(userData.name)")
|
||||||
|
.font(.nps(font: .bold, size: 20))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
VStack(alignment: .center, spacing: 0) {
|
||||||
|
SimpleBtnView(vm: btnVM, id: notifyBtnID)
|
||||||
|
Text("알림설정")
|
||||||
|
.font(.nps(font: .bold, size: 8))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("ID")
|
||||||
|
.font(.nps(font: .bold, size: 16))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
Text("\(userData.userID)")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
}
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("E-mail")
|
||||||
|
.font(.nps(font: .bold, size: 16))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
Spacer(minLength: 1)
|
||||||
|
Text("\(userData.email)")
|
||||||
|
.font(.nps(size: 16))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
.multilineStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.foregroundStyle(Color(.Other.cell))
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
btnVM.setImage(for: notifyBtnID, newImage: Image(.Icon.notificationSET))
|
||||||
|
btnVM.setSize(for: notifyBtnID, newWidth: 24, newHeight: 24)
|
||||||
|
btnVM.setAction(for: notifyBtnID) {
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 알림 설정 페이지로 이동
|
||||||
|
printLog("알림 설정 페이지")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
44
AcaMate/1. View/12. Main/125. Etc/UserSettingView.swift
Normal file
44
AcaMate/1. View/12. Main/125. Etc/UserSettingView.swift
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
//
|
||||||
|
// UserSettingView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/13/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct UserSettingView: View {
|
||||||
|
var myType: UserType
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
EtcBoxView(title: "설정") {
|
||||||
|
VStack(spacing: 0){
|
||||||
|
EtcCellView(title: "정보 변경") {
|
||||||
|
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 정보 변경 이동
|
||||||
|
printLog("정보 변경 이동")
|
||||||
|
}
|
||||||
|
DashedDivider()
|
||||||
|
EtcCellView(title: "계정 관리") {
|
||||||
|
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 계정 관리 이동
|
||||||
|
printLog("계정 관리 이동")
|
||||||
|
}
|
||||||
|
if myType == .Admin {
|
||||||
|
DashedDivider()
|
||||||
|
EtcCellView(title: "관리자 페이지") {
|
||||||
|
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 이거 분기 쳐서 특정 사용자 아니면 접근 못하게 막기
|
||||||
|
|
||||||
|
printLog("관리자 페이지 이동")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ struct BottomView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(EdgeInsets(top: 12, leading: 24, bottom: 0, trailing: 24))
|
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||||
|
|
||||||
.background {
|
.background {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
|
|
26
AcaMate/1. View/12. Main/EmptyBoxView.swift
Normal file
26
AcaMate/1. View/12. Main/EmptyBoxView.swift
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
//
|
||||||
|
// EmptyBoxView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/14/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EmptyBoxView: View {
|
||||||
|
let title: String
|
||||||
|
var body: some View {
|
||||||
|
Text("\(title)")
|
||||||
|
.font(.nps(size: 20))
|
||||||
|
.foregroundStyle(Color(.Text.detail))
|
||||||
|
.multilineStyle()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding([.top,.bottom],12)
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.stroke(Color(.Second.normal), lineWidth: 2)
|
||||||
|
.fill(Color(.Second.light))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,9 +10,10 @@ import Combine
|
||||||
|
|
||||||
struct MainView: View {
|
struct MainView: View {
|
||||||
@EnvironmentObject var appVM: AppViewModel
|
@EnvironmentObject var appVM: AppViewModel
|
||||||
@EnvironmentObject var alertController: AlertController
|
|
||||||
@State var cancellables: Set<AnyCancellable> = []
|
@State var cancellables: Set<AnyCancellable> = []
|
||||||
@Binding var naviState : NaviState
|
// @Binding var naviState : NaviState
|
||||||
|
|
||||||
|
@State private var myType: UserType = .Admin
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
|
@ -20,18 +21,18 @@ struct MainView: View {
|
||||||
Group {
|
Group {
|
||||||
switch appVM.menuName {
|
switch appVM.menuName {
|
||||||
case .Home:
|
case .Home:
|
||||||
HomeView()
|
HomeView(myType: $myType)
|
||||||
case .Management:
|
case .Management:
|
||||||
ManagementView()
|
ManagementView()
|
||||||
case .Chatting:
|
case .Chatting:
|
||||||
ChattingView()
|
ChattingView(appVM, $myType)
|
||||||
case .Calendar:
|
case .Calendar:
|
||||||
CalendarView()
|
CalendarView()
|
||||||
case .Etc:
|
case .Etc:
|
||||||
EtcView()
|
EtcView(myType: $myType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(minLength: 1)
|
// Spacer(minLength: 1)
|
||||||
|
|
||||||
BottomView()
|
BottomView()
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|
|
@ -8,37 +8,36 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TopView: View {
|
struct TopView: View {
|
||||||
@StateObject var btnVM = ButtonViewModel()
|
@ObservedObject var topVM: TopViewModel
|
||||||
|
|
||||||
@State var titleName: String = ""
|
|
||||||
|
|
||||||
@State private var leftBtnID = UUID()
|
|
||||||
@State private var rightBtnID = UUID()
|
|
||||||
|
|
||||||
//MARK: - 변경 값
|
|
||||||
var myType: UserType = .Student
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .center, spacing: 0) {
|
HStack(alignment: .center, spacing: 12) {
|
||||||
SimpleBtnView(vm: btnVM, id: leftBtnID)
|
SimpleBtnView(vm: topVM.btnVM, id: topVM.leftBtnID)
|
||||||
.background {
|
.background {
|
||||||
if let state = btnVM.btnStates[leftBtnID], state.image == nil {
|
if let state = topVM.btnVM.btnStates[topVM.leftBtnID], state.image == nil, state.text != nil {
|
||||||
Circle()
|
Circle()
|
||||||
.strokeBorder(Color(.Second.normal) ,lineWidth: 4)
|
.strokeBorder(Color(.Second.normal) ,lineWidth: 4)
|
||||||
.frame(width: 40, height: 40)
|
.frame(width: state.width, height: state.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 12))
|
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 0))
|
||||||
|
|
||||||
|
|
||||||
Text("\(titleName)")
|
Text("\(topVM.titleName)")
|
||||||
.foregroundStyle(Color(.Text.detail))
|
.foregroundStyle(Color(.Text.detail))
|
||||||
.font(.nps(font: .bold, size: 20))
|
.font(.nps(font: .bold, size: 20))
|
||||||
Spacer()
|
.frame(height: 40)
|
||||||
|
.padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12))
|
||||||
SimpleBtnView(vm: btnVM, id: rightBtnID)
|
Spacer(minLength: 1)
|
||||||
.padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 24))
|
|
||||||
|
|
||||||
|
SimpleBtnView(vm: topVM.btnVM, id: topVM.rightBtnID)
|
||||||
|
.background {
|
||||||
|
if let state = topVM.btnVM.btnStates[topVM.rightBtnID], state.image == nil, state.text != nil {
|
||||||
|
Circle()
|
||||||
|
.strokeBorder(Color(.Second.normal) ,lineWidth: 4)
|
||||||
|
.frame(width: state.width, height: state.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 24))
|
||||||
}
|
}
|
||||||
.background {
|
.background {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
|
@ -47,48 +46,9 @@ struct TopView: View {
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
btnVM.btnStates[leftBtnID] = ButtonState()
|
|
||||||
btnVM.btnStates[rightBtnID] = ButtonState()
|
|
||||||
|
|
||||||
btnVM.setSize(for: leftBtnID, newWidth: 40, newHeight: 40)
|
|
||||||
btnVM.setSize(for: rightBtnID, newWidth: 40, newHeight: 40)
|
|
||||||
|
|
||||||
if self.myType == .Student {
|
|
||||||
btnVM.setImage(for: leftBtnID, newImage: Image(.Icon.face))
|
|
||||||
} else {
|
|
||||||
btnVM.setText(for: leftBtnID,
|
|
||||||
newText: "\(myType.rawValue)",
|
|
||||||
newFont: .nps(font: .bold, size: 24))
|
|
||||||
}
|
|
||||||
btnVM.setImage(for: rightBtnID, newImage: Image(.Icon.notificationSET))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//struct TypeIcon: View {
|
|
||||||
// var myType: UserType
|
|
||||||
//
|
|
||||||
// var body: some View {
|
|
||||||
|
|
||||||
// if self.myType == .Student {
|
|
||||||
|
|
||||||
// SimpleBtnView(image: Image(.Icon.face), title: nil, font: nil, width: 40, height: 40)
|
|
||||||
// } else {
|
|
||||||
// SimpleBtnView(image: nil, title: "\(self.myType.rawValue)", font: .nps(font: .bold, size: 24), width: 40, height: 40)
|
|
||||||
// .doAction {
|
|
||||||
// printLog("CHECK!!!")
|
|
||||||
// }
|
|
||||||
// .setTextColor(.red)
|
|
||||||
// .setIsUsable(false)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
//#Preview {
|
|
||||||
// TopView(titleName: "Name")
|
|
||||||
//}
|
|
||||||
|
|
29
AcaMate/2. Model/API Request.swift
Normal file
29
AcaMate/2. Model/API Request.swift
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
//
|
||||||
|
// API Request.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 3/18/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Alamofire
|
||||||
|
|
||||||
|
public struct APIRequest<T: APIResponseProtocol> {
|
||||||
|
let url, path: String
|
||||||
|
let method: HTTPMethod
|
||||||
|
var parameters: [String: Any]
|
||||||
|
let headers: HTTPHeaders
|
||||||
|
let decoding: T.Type
|
||||||
|
|
||||||
|
init(url: String = "\(API_URL)", path: String,
|
||||||
|
method: HTTPMethod = .get, headers: HTTPHeaders = [:],
|
||||||
|
parameters: [String : Any] = [:], decoding: T.Type) {
|
||||||
|
self.url = url
|
||||||
|
self.path = path
|
||||||
|
self.method = method
|
||||||
|
self.headers = headers
|
||||||
|
self.parameters = parameters
|
||||||
|
self.decoding = decoding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,30 +6,98 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
public protocol APIResponseProtocol: Decodable {
|
||||||
class APIResponse<T: Codable>: Codable {
|
var status: Status { get }
|
||||||
let status: Status
|
|
||||||
let data: T
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Status: Codable {
|
class APIResponse<T: Codable>: Codable, APIResponseProtocol {
|
||||||
let code: String
|
let status: Status
|
||||||
|
let data: T?
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Status: Codable {
|
||||||
|
let code: APICode
|
||||||
let message: String
|
let message: String
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------
|
enum APICode: Codable, RawRepresentable {
|
||||||
|
case success(String)
|
||||||
|
case inputErr(String)
|
||||||
|
case outputErr(String)
|
||||||
|
case networkErr(String)
|
||||||
|
case unknownErr(String)
|
||||||
|
case anything(String)
|
||||||
|
|
||||||
|
var rawValue: String {
|
||||||
|
switch self {
|
||||||
|
case .success(let value),
|
||||||
|
.inputErr(let value),
|
||||||
|
.outputErr(let value),
|
||||||
|
.networkErr(let value),
|
||||||
|
.unknownErr(let value),
|
||||||
|
.anything(let value):
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(rawValue: String){
|
||||||
|
if rawValue.hasPrefix("0") {self = .success(rawValue)}
|
||||||
|
else if rawValue.hasPrefix("1") {self = .inputErr(rawValue)}
|
||||||
|
else if rawValue.hasPrefix("2") {self = .outputErr(rawValue)}
|
||||||
|
else if rawValue.hasPrefix("3") {self = .networkErr(rawValue)}
|
||||||
|
else if rawValue == "999" {self = .unknownErr(rawValue)}
|
||||||
|
else { self = .anything(rawValue)}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
let rawValue = try container.decode(String.self)
|
||||||
|
self = APICode(rawValue: rawValue) ?? .anything(rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
try container.encode(self.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// /api/v1/in/app ----------------
|
||||||
|
class Header: Codable {
|
||||||
|
let header: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/v1/in/app/version ----------------
|
||||||
class VersionData: Codable {
|
class VersionData: Codable {
|
||||||
let os_type, final_ver, dev_ver, force_ver: String
|
let os_type, final_ver, dev_ver, force_ver: String
|
||||||
let choice_update_yn: Bool
|
let choice_update_yn: Bool
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------
|
// /api/v1/in/app/retryAccess ----------------
|
||||||
|
class Access: Codable {
|
||||||
|
let access: String
|
||||||
|
}
|
||||||
|
|
||||||
class User_Academy: Codable {
|
// /api/v1/in/user ----------------
|
||||||
let uid: String
|
class User: Codable {
|
||||||
let bid: [String]
|
let uid, name, type, login_date: String
|
||||||
|
let device_id, push_token, birth: String?
|
||||||
|
let auto_login_yn: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/v1/in/user/login ----------------
|
||||||
|
// /api/v1/in/user/register ----------------
|
||||||
|
class User_Token: Codable {
|
||||||
|
let token: String?
|
||||||
|
let refresh: String?
|
||||||
|
// let bids: [String]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// /api/v1/in/member/academy ----------------
|
||||||
|
class AcademyName: Codable {
|
||||||
|
let bid: String?
|
||||||
|
let name: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,18 @@ struct SetAlertData {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 서버에서 발생한 크리티컬한 오류 - 앱 종료가 최선
|
||||||
|
func setServerError(action: CurrentValueSubject<String?, Never>) -> AlertData {
|
||||||
|
return AlertData(title: "시스템 오류", body: "시스템이 정상적이지 않습니다. \n확인 후 다시 시도해주세요.",
|
||||||
|
button: [
|
||||||
|
ButtonType(name: "확인", role: .cancel,
|
||||||
|
function: {
|
||||||
|
printLog("alertAction 'exit' send 실행됨")
|
||||||
|
action.send("exit")
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
/// 로그인 문제 발생
|
/// 로그인 문제 발생
|
||||||
func setErrorLogin() -> AlertData {
|
func setErrorLogin() -> AlertData {
|
||||||
return AlertData(title: "로그인",
|
return AlertData(title: "로그인",
|
||||||
|
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
||||||
struct ButtonState {
|
struct ButtonState {
|
||||||
var image: Image? = nil
|
var image: Image? = nil
|
||||||
|
|
||||||
var title: String? = nil
|
var text: String? = nil
|
||||||
var font: Font? = nil
|
var font: Font? = nil
|
||||||
|
|
||||||
var width: CGFloat = 0
|
var width: CGFloat = 0
|
||||||
|
|
36
AcaMate/2. Model/Chat Data.swift
Normal file
36
AcaMate/2. Model/Chat Data.swift
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
//
|
||||||
|
// Chat Data.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/14/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum chatType {
|
||||||
|
case Class
|
||||||
|
case Student
|
||||||
|
case Parent
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SummaryChat {
|
||||||
|
var id: String
|
||||||
|
|
||||||
|
var chatName: String
|
||||||
|
var teacherName: String
|
||||||
|
var lastMessage: String
|
||||||
|
var dayDate: String
|
||||||
|
var timeDate: String
|
||||||
|
|
||||||
|
var notiState: Bool
|
||||||
|
|
||||||
|
var groupNum: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatMesage {
|
||||||
|
var roomID: String
|
||||||
|
var chatID: String
|
||||||
|
var senderID: String
|
||||||
|
var message: String
|
||||||
|
var sendTime: String
|
||||||
|
}
|
16
AcaMate/2. Model/CustomError.swift
Normal file
16
AcaMate/2. Model/CustomError.swift
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
//
|
||||||
|
// CustomError.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 3/14/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class ACA_ERROR: Error {
|
||||||
|
let message: String
|
||||||
|
|
||||||
|
init(_ msg: String) {
|
||||||
|
message = msg
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,8 +13,6 @@ struct NaviState: Equatable {
|
||||||
var act: NaviAction
|
var act: NaviAction
|
||||||
var path: PathName
|
var path: PathName
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static func == (lhs: NaviState, rhs: NaviState) -> Bool {
|
static func == (lhs: NaviState, rhs: NaviState) -> Bool {
|
||||||
return lhs.act == rhs.act && lhs.path == rhs.path
|
return lhs.act == rhs.act && lhs.path == rhs.path
|
||||||
}
|
}
|
||||||
|
@ -44,8 +42,10 @@ enum NaviAction: Hashable {
|
||||||
enum PathName: Hashable {
|
enum PathName: Hashable {
|
||||||
case Intro
|
case Intro
|
||||||
case Login
|
case Login
|
||||||
|
case Register(_ type: SNSLoginType, id: String)
|
||||||
|
case SelectAcademy
|
||||||
case Main
|
case Main
|
||||||
|
case ChatRoom(id: String)
|
||||||
case NONE
|
case NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,10 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
enum SNSLoginType{
|
enum SNSLoginType: String{
|
||||||
case Kakao
|
case Apple = "ST00"
|
||||||
case Apple
|
case Kakao = "ST01"
|
||||||
|
case Dev = "ST02"
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SNSID: Codable {
|
struct SNSID: Codable {
|
||||||
|
|
62
AcaMate/2. Model/User Data.swift
Normal file
62
AcaMate/2. Model/User Data.swift
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
//
|
||||||
|
// UserType.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/5/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum UserType: String {
|
||||||
|
case Admin
|
||||||
|
case Employee
|
||||||
|
case Student
|
||||||
|
case Teacher
|
||||||
|
case Parent
|
||||||
|
case ETC
|
||||||
|
|
||||||
|
var code: String {
|
||||||
|
switch self {
|
||||||
|
case .Admin: return "UT00"
|
||||||
|
case .Employee: return "UT01"
|
||||||
|
case .Student: return "UT02"
|
||||||
|
case .Teacher: return "UT03"
|
||||||
|
case .Parent: return "UT04"
|
||||||
|
case .ETC: return "UT05"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var name: String {
|
||||||
|
switch self {
|
||||||
|
case .Admin: return "A"
|
||||||
|
case .Employee: return "E"
|
||||||
|
case .Student: return "S"
|
||||||
|
case .Teacher: return "T"
|
||||||
|
case .Parent: return "P"
|
||||||
|
case .ETC: return "V"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SummaryUser {
|
||||||
|
var profile: Image
|
||||||
|
var name: String
|
||||||
|
var userID: String
|
||||||
|
var email: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RegisterUserInfo {
|
||||||
|
var name: String
|
||||||
|
var birth: Date
|
||||||
|
var type: String = "UT05"
|
||||||
|
var device_id: String // 진짜 디바이스 아이디
|
||||||
|
var auto_login_yn: String
|
||||||
|
var push_token: String // APNs 용 토큰
|
||||||
|
var email: String
|
||||||
|
var phone: String
|
||||||
|
var address: String
|
||||||
|
var sns_id: String
|
||||||
|
var sns_type: String
|
||||||
|
var location_yn, camera_yn, photo_yn, push_yn: Bool?
|
||||||
|
var market_app_yn, market_sms_yn, market_email_yn: Bool?
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
//
|
|
||||||
// UserType.swift
|
|
||||||
// AcaMate
|
|
||||||
//
|
|
||||||
// Created by TAnine on 2/5/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
enum UserType: String {
|
|
||||||
case Student = "S"
|
|
||||||
case Parent = "P"
|
|
||||||
case Teacher = "T"
|
|
||||||
case Admin = "A"
|
|
||||||
case Employee = "E"
|
|
||||||
case ETC = "V"
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
//
|
|
||||||
// AlertController.swift
|
|
||||||
// AcaMate
|
|
||||||
//
|
|
||||||
// Created by Sean Kim on 12/13/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
|
|
||||||
class AlertViewModel2: ObservableObject {
|
|
||||||
@Published var showAlert: Bool = false
|
|
||||||
var alertData: AlertData = .init(body: "")
|
|
||||||
|
|
||||||
let alertAction = CurrentValueSubject<String?, Never>(nil)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -7,14 +7,30 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Combine
|
import Combine
|
||||||
|
//
|
||||||
|
//import AVFoundation
|
||||||
|
//import Photos
|
||||||
|
//import CoreLocation
|
||||||
|
//import UserNotifications
|
||||||
|
|
||||||
class AppViewModel: ObservableObject {
|
class AppViewModel: ObservableObject {
|
||||||
|
// public static let shared = AppViewModel()
|
||||||
|
|
||||||
@Published var isLoading: Bool = false
|
@Published var isLoading: Bool = false
|
||||||
@Published var showAlert: Bool = false
|
@Published var showAlert: Bool = false
|
||||||
@Published var menuName: MenuName = .Home
|
@Published var menuName: MenuName = .Home
|
||||||
|
@Published var naviState: NaviState = .init(act: .NONE, path: .Intro)
|
||||||
|
|
||||||
var alertData: AlertData = .init(body: "")
|
@Published var alertData: AlertData = .init(body: "")
|
||||||
|
|
||||||
|
/// 항상 최신값을 가지고 있다가 구독자 추가 되면 그 즉시 값을 전달하고 이후 업데이트 되는 값을 계속 보내주는 역할을 함
|
||||||
let alertAction = CurrentValueSubject<String?, Never>(nil)
|
let alertAction = CurrentValueSubject<String?, Never>(nil)
|
||||||
|
var apiManager: APIManager = APIManager()
|
||||||
|
var permissionManager = PermissionManager()
|
||||||
|
|
||||||
|
// init() {
|
||||||
|
// permissionManager.location
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ class ButtonViewModel: ObservableObject {
|
||||||
func setText(for id: UUID, newText: String?, newFont: Font?) {
|
func setText(for id: UUID, newText: String?, newFont: Font?) {
|
||||||
var state = btnStates[id] ?? ButtonState()
|
var state = btnStates[id] ?? ButtonState()
|
||||||
|
|
||||||
state.title = newText
|
state.text = newText
|
||||||
state.font = newFont
|
state.font = newFont
|
||||||
|
|
||||||
btnStates[id] = state
|
btnStates[id] = state
|
||||||
|
|
18
AcaMate/3. ViewModel/ChatViewModel.swift
Normal file
18
AcaMate/3. ViewModel/ChatViewModel.swift
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
//
|
||||||
|
// ChatViewModel.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/17/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
class ChatViewModel: ObservableObject {
|
||||||
|
private let appVM: AppViewModel
|
||||||
|
|
||||||
|
@Published var messages: [ChatMesage] = []
|
||||||
|
|
||||||
|
init(_ appVM: AppViewModel) {
|
||||||
|
self.appVM = appVM
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
164
AcaMate/3. ViewModel/IntroViewModel.swift
Normal file
164
AcaMate/3. ViewModel/IntroViewModel.swift
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
//
|
||||||
|
// IntroViewModel.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 3/20/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
|
||||||
|
class IntroViewModel: ObservableObject {
|
||||||
|
private let appVM: AppViewModel
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@UserDefault(key: "header", defaultValue: "headerValue") var headerValue
|
||||||
|
|
||||||
|
init(_ appVM: AppViewModel) {
|
||||||
|
self.appVM = appVM
|
||||||
|
}
|
||||||
|
|
||||||
|
func appStart() {
|
||||||
|
subscribeAlertAction()
|
||||||
|
searchHeader()
|
||||||
|
.flatMap { success -> Future<VersionData, Error> in
|
||||||
|
return self.loadVersion()
|
||||||
|
}
|
||||||
|
.sink { [weak self] completion in
|
||||||
|
guard let self = self else {return}
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
// 만약 여기서 에러가 난다면 서버에 문제가 있다는 것
|
||||||
|
printLog(error)
|
||||||
|
self.appVM.alertData = SetAlertData().setServerError(action: appVM.alertAction)
|
||||||
|
self.appVM.showAlert.toggle()
|
||||||
|
case .finished: break
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] version in
|
||||||
|
guard let self = self else {return}
|
||||||
|
@UserDefault(key:"currentVer", defaultValue: "0.0.0") var currentVer
|
||||||
|
@UserDefault(key:"finalVer", defaultValue: "0.0.0") var finalVer
|
||||||
|
currentVer = currentVersion()
|
||||||
|
finalVer = version.final_ver
|
||||||
|
|
||||||
|
let compareForce = compareVersion(version.force_ver, currentVer)//currentVersion())
|
||||||
|
let compareChoice = compareVersion(version.final_ver, currentVer)//currentVersion())
|
||||||
|
|
||||||
|
if compareForce == .bigger {
|
||||||
|
appVM.alertData = SetAlertData().setForceUpdate(
|
||||||
|
action: appVM.alertAction
|
||||||
|
)
|
||||||
|
appVM.showAlert.toggle()
|
||||||
|
} else if compareChoice == .bigger && version.choice_update_yn {
|
||||||
|
appVM.alertData = SetAlertData().setSelectUpdate(
|
||||||
|
action: appVM.alertAction
|
||||||
|
)
|
||||||
|
appVM.showAlert.toggle()
|
||||||
|
} else {
|
||||||
|
// 정상 동작 넘어감
|
||||||
|
appVM.naviState.set(act: .RESET, path: .Login)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchHeader() -> Future<Bool, Error> {
|
||||||
|
return Future { [weak self] promise in
|
||||||
|
|
||||||
|
guard let self = self else {
|
||||||
|
promise(.failure(ACA_ERROR("Self 전환 오류")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let deviceId = UIDevice.current.identifierForVendor?.uuidString,
|
||||||
|
let bundleId = Bundle.main.bundleIdentifier else {
|
||||||
|
promise(.failure(ACA_ERROR("번들/디바이스 아이디 조회 불량")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = APIRequest(path: "/api/v1/in/app",
|
||||||
|
parameters: ["type": "I", "specific": deviceId, "project": bundleId],
|
||||||
|
decoding: APIResponse<Header>.self)
|
||||||
|
appVM.apiManager.loadAPIData(request)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
printLog("최종 에러: \(error)")
|
||||||
|
promise(.failure(error))
|
||||||
|
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
guard let self = self else {return}
|
||||||
|
if let data = response.data {
|
||||||
|
self.headerValue = data.header
|
||||||
|
promise(.success(true))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
promise(.failure(ACA_ERROR("데이터 없음")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
}
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func subscribeAlertAction() {
|
||||||
|
appVM.alertAction
|
||||||
|
.compactMap { $0 }
|
||||||
|
.sink { [weak self] action in
|
||||||
|
guard let self = self else {return}
|
||||||
|
if action == "exit" {
|
||||||
|
exit(1)
|
||||||
|
} else if action == "updateNow" {
|
||||||
|
exit(1)
|
||||||
|
//MARK: - TODO (앱스토어 이동 로직 넣을 것)
|
||||||
|
} else {
|
||||||
|
appVM.naviState.set(act: .RESET, path: .Login)
|
||||||
|
}
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadVersion() -> Future<VersionData, Error> {
|
||||||
|
return Future { [weak self] promise in
|
||||||
|
guard let self = self else {return}
|
||||||
|
let request = APIRequest(path: "/api/v1/in/app/version",
|
||||||
|
headers: [API_HEADER : self.headerValue],
|
||||||
|
parameters: ["type":"I"],
|
||||||
|
decoding: APIResponse<VersionData>.self)
|
||||||
|
|
||||||
|
appVM.apiManager.loadAPIData(request)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
printLog("\(error)")
|
||||||
|
promise(.failure(error))
|
||||||
|
case .finished: break
|
||||||
|
}
|
||||||
|
} receiveValue: { versionData in
|
||||||
|
guard let version = versionData.data else {return}
|
||||||
|
printLog("\(version.toStringDict())")
|
||||||
|
promise(.success(version))
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private func currentVersion() -> String {
|
||||||
|
guard let currentVer = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { return "" }
|
||||||
|
return currentVer
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private func versionChange(ver: String) -> [Int] {
|
||||||
|
return ver.components(separatedBy: ["."]).map {Int($0) ?? 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -9,5 +9,87 @@ import SwiftUI
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class LoginViewModel: ObservableObject {
|
class LoginViewModel: ObservableObject {
|
||||||
let loginAction = CurrentValueSubject<Bool, Never>(false)
|
private let appVM: AppViewModel
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// @Published var toggleLoading: Bool = false
|
||||||
|
// @Published var pathName: PathName = .NONE
|
||||||
|
|
||||||
|
@UserDefault(key: "token", defaultValue: "accToken") var accToken
|
||||||
|
@UserDefault(key: "refresh", defaultValue: "refreshToken") var refresh
|
||||||
|
@UserDefault(key: "header", defaultValue: "headerValue") var headerValue
|
||||||
|
|
||||||
|
@Published var devId: String = ""
|
||||||
|
|
||||||
|
var bidArray: [String] = []
|
||||||
|
|
||||||
|
|
||||||
|
init(_ appVM: AppViewModel) {
|
||||||
|
self.appVM = appVM
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginAction(type: SNSLoginType) {
|
||||||
|
appVM.isLoading = true
|
||||||
|
let acctype: String = type == .Apple ? "ST00": (type == .Kakao ? "ST01" : "ST02")
|
||||||
|
LoginController().login(type, devId)
|
||||||
|
.flatMap{ snsId in
|
||||||
|
self.appVM.apiManager.loadAPIData(
|
||||||
|
APIRequest(path: "/api/v1/in/user/login",
|
||||||
|
headers: [API_HEADER : self.headerValue],
|
||||||
|
parameters: [
|
||||||
|
"acctype": acctype,
|
||||||
|
"snsId": "\(snsId.snsId)"
|
||||||
|
],
|
||||||
|
decoding: APIResponse<User_Token>.self))
|
||||||
|
.map { response in
|
||||||
|
return (snsId: snsId.snsId, response: response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sink { [weak self] completion in
|
||||||
|
guard let self = self else { return }
|
||||||
|
// API 자체적으로 내보내는 에러는 여기서 거를거고
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
self.appVM.isLoading = false
|
||||||
|
printLog("\(error)")
|
||||||
|
case .finished:
|
||||||
|
self.appVM.isLoading = false
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let snsId = response.snsId
|
||||||
|
switch response.response.status.code {
|
||||||
|
case .success(let code):
|
||||||
|
if code == "000" {
|
||||||
|
if let data = response.response.data,
|
||||||
|
let accToken = data.token,
|
||||||
|
let refresh = data.refresh {
|
||||||
|
printLog(accToken)
|
||||||
|
printLog(refresh)
|
||||||
|
|
||||||
|
self.accToken = accToken
|
||||||
|
self.refresh = refresh
|
||||||
|
|
||||||
|
appVM.naviState.set(act: .ADD, path: .SelectAcademy)
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 회원가입 진행
|
||||||
|
// 여기다가 타입도 추가해야 함
|
||||||
|
appVM.naviState.set(act: .ADD, path: .Register(type, id: "\(snsId)"))
|
||||||
|
}
|
||||||
|
case .anything(let apiCode):
|
||||||
|
// MARK: TO-DO
|
||||||
|
// 이거도 걍 에러인데? 에러 처리 로직으로
|
||||||
|
printLog("\(apiCode) : 로그인 정보 없음")
|
||||||
|
printLog("ERROR")
|
||||||
|
// self.pathName = .Register(type, id: "\(id)")
|
||||||
|
default:
|
||||||
|
// 그외에 서버에서 처리를 하다가 문제가 생겨서 발생하는 에러는 여기로 보낼거임
|
||||||
|
printLog("ERROR")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
168
AcaMate/3. ViewModel/RegisterViewModel.swift
Normal file
168
AcaMate/3. ViewModel/RegisterViewModel.swift
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
//
|
||||||
|
// RegisterViewModel.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 3/24/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class RegisterViewModel: ObservableObject {
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private let appVM: AppViewModel
|
||||||
|
private let responseValue: (SNSLoginType, String)
|
||||||
|
|
||||||
|
init(_ appVM: AppViewModel, type: SNSLoginType, snsID: String) {
|
||||||
|
self.appVM = appVM
|
||||||
|
self.responseValue = (type, snsID)
|
||||||
|
}
|
||||||
|
|
||||||
|
@UserDefault(key: "token", defaultValue: "accToken") var accToken
|
||||||
|
@UserDefault(key: "refresh", defaultValue: "refreshToken") var refresh
|
||||||
|
@UserDefault(key: "header", defaultValue: "headerValue") var headerValue
|
||||||
|
@UserDefault(key: "pushToken", defaultValue: "pushToken") var pushToken
|
||||||
|
|
||||||
|
let addressBtnID = UUID()
|
||||||
|
let registerBtnID = UUID()
|
||||||
|
let policyBtn1ID = UUID()
|
||||||
|
|
||||||
|
@Published var selectDate: Date = {
|
||||||
|
let calendar = Calendar.current
|
||||||
|
return calendar.date(byAdding: .year, value: 0, to: Date()) ?? Date()
|
||||||
|
}() {
|
||||||
|
didSet {
|
||||||
|
if selectDate != oldValue {
|
||||||
|
changeDate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var changeDate: Bool = false
|
||||||
|
|
||||||
|
@Published var nameText: String = ""
|
||||||
|
@Published var emailFrontText: String = ""
|
||||||
|
@Published var emailTailText: String = ""
|
||||||
|
@Published var numberHead: String = ""
|
||||||
|
@Published var phoneTextSet: (String,String,String) = ("","","")
|
||||||
|
@Published var addressText: String = "주소 입력"
|
||||||
|
@Published var addrDetailText: String = ""
|
||||||
|
|
||||||
|
let numberHeadList = ["010","011","016","017","018","019"]
|
||||||
|
let emailTailList = ["gmail.com",
|
||||||
|
"naver.com",
|
||||||
|
"daum.net",
|
||||||
|
"hanmail.net",
|
||||||
|
"nate.com",
|
||||||
|
"outlook.com",
|
||||||
|
"icloud.com",
|
||||||
|
"kakao.com",
|
||||||
|
"yahoo.com",
|
||||||
|
"protonmail.com",
|
||||||
|
"직접 입력"]
|
||||||
|
|
||||||
|
func registerUser() async {
|
||||||
|
// 필수 값
|
||||||
|
if nameText != "" && emailFrontText != "" && emailTailText != "" && phoneTextSet.0 != "" && phoneTextSet.1 != "" && phoneTextSet.2 != "" {
|
||||||
|
var param: [String:Any] = [:]
|
||||||
|
|
||||||
|
param["name"] = "\(nameText)"
|
||||||
|
if changeDate { param["birth"] = "\(selectDate.convertString("yyyy-MM-dd"))"}
|
||||||
|
param["type"] = UserType.ETC.code //"UT05" // 일반 유저
|
||||||
|
|
||||||
|
if let deviceId = await UIDevice.current.identifierForVendor?.uuidString {
|
||||||
|
param["device_id"] = "\(deviceId)"
|
||||||
|
}
|
||||||
|
|
||||||
|
param["auto_login_yn"] = false
|
||||||
|
param["login_date"] = Date().convertString("yyyy-MM-dd'T'HH:mm:ss")
|
||||||
|
param["push_token"] = "\(pushToken)"
|
||||||
|
param["email"] = "\(emailFrontText)@\(emailTailText)"
|
||||||
|
|
||||||
|
param["phone"] = "\(phoneTextSet.0)\(phoneTextSet.1)\(phoneTextSet.2)"
|
||||||
|
if addressText != "주소 입력" {
|
||||||
|
if addrDetailText == "" { param["address"] = "\(self.addressText)" }
|
||||||
|
else { param["address"] = "\(self.addressText) \(self.addrDetailText)"}
|
||||||
|
}
|
||||||
|
param["location_yn"] = appVM.permissionManager.checkLocationPermission()
|
||||||
|
param["camera_yn"] = appVM.permissionManager.checkCameraPermission()
|
||||||
|
param["photo_yn"] = appVM.permissionManager.checkPhotoPermission()
|
||||||
|
appVM.permissionManager.checkPushPermission(completion: { status in
|
||||||
|
param["push_yn"] = status
|
||||||
|
})
|
||||||
|
|
||||||
|
param["market_app_yn"] = true
|
||||||
|
param["market_sms_yn"] = true
|
||||||
|
param["market_email_yn"] = true
|
||||||
|
|
||||||
|
param["sns_id"] = self.responseValue.1
|
||||||
|
param["sns_type"] = self.responseValue.0.rawValue
|
||||||
|
|
||||||
|
|
||||||
|
if !changeDate || addressText == "주소 입력" {
|
||||||
|
appVM.alertData = AlertData(
|
||||||
|
title: "알림",
|
||||||
|
body: "\(changeDate ? "":"[생일]")\((addressText != "주소 입력") ? "":"[주소]")의 내용이 없습니다.\n 계속해서 진행할까요?",
|
||||||
|
button: [
|
||||||
|
ButtonType(name: "돌아가기", role: .destructive, function: nil),
|
||||||
|
ButtonType(name: "가입하기", role: .cancel) {
|
||||||
|
self.callAPI(param)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
appVM.showAlert.toggle()
|
||||||
|
// 넘어가는 로직도 추가
|
||||||
|
} else {
|
||||||
|
self.callAPI(param)
|
||||||
|
//정상 동작
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
appVM.alertData = AlertData(
|
||||||
|
title: "경고", body: "필수 입력 사항이 누락되었습니다.",
|
||||||
|
button: [ButtonType(name: "확인", role: .cancel, function: nil)])
|
||||||
|
appVM.showAlert.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func callAPI(_ param: [String: Any]) {
|
||||||
|
|
||||||
|
self.appVM.apiManager.loadAPIData(
|
||||||
|
APIRequest(path: "/api/v1/in/user/register",
|
||||||
|
method: .post,
|
||||||
|
headers: [API_HEADER : self.headerValue],
|
||||||
|
parameters: param,
|
||||||
|
decoding: APIResponse<User_Token>.self)
|
||||||
|
)
|
||||||
|
.sink { [weak self] completion in
|
||||||
|
guard let self = self else { return }
|
||||||
|
// API 자체적으로 내보내는 에러는 여기서 거를거고
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
printLog("\(error)")
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
switch response.status.code {
|
||||||
|
case .success(let code):
|
||||||
|
if code == "000" {
|
||||||
|
if let data = response.data, let accToken = data.token, let refresh = data.refresh{
|
||||||
|
self.accToken = accToken
|
||||||
|
self.refresh = refresh
|
||||||
|
|
||||||
|
appVM.naviState.set(act: .ADD, path: .SelectAcademy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
62
AcaMate/3. ViewModel/SelectAcademyViewModel.swift
Normal file
62
AcaMate/3. ViewModel/SelectAcademyViewModel.swift
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
//
|
||||||
|
// SelectAcademyViewModel.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/19/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class SelectAcademyViewModel: ObservableObject {
|
||||||
|
private var appVM: AppViewModel
|
||||||
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
|
init(_ appVM: AppViewModel) {
|
||||||
|
self.appVM = appVM
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var academyCode: String = ""
|
||||||
|
@Published var academyList: [AcademyName] = []
|
||||||
|
@Published var selectNum: Int = -1
|
||||||
|
|
||||||
|
func moveChatting() {
|
||||||
|
appVM.naviState.set(act: .RESET, path: .Main)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func loadAcademy() {
|
||||||
|
@UserDefault(key: "token", defaultValue: "accToken") var token
|
||||||
|
@UserDefault(key: "refresh", defaultValue: "refreshToken") var refresh
|
||||||
|
|
||||||
|
let request = APIRequest(path: "/api/v1/in/user/academy",
|
||||||
|
parameters: ["token": token, "refresh": refresh],
|
||||||
|
decoding: APIResponse<[AcademyName]>.self)
|
||||||
|
|
||||||
|
appVM.apiManager.loadAPIData(request)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
printLog("\(error)")
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
guard let self = self else {return}
|
||||||
|
guard let academyNames = response as? APIResponse<[AcademyName]> else {return}
|
||||||
|
guard let academyList = academyNames.data else { return }
|
||||||
|
self.academyList = academyList
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleSelection(for index: Int){
|
||||||
|
if selectNum == index {
|
||||||
|
selectNum = -1
|
||||||
|
} else {
|
||||||
|
selectNum = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
44
AcaMate/3. ViewModel/TopViewModel.swift
Normal file
44
AcaMate/3. ViewModel/TopViewModel.swift
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
//
|
||||||
|
// TopViewModel.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/13/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class TopViewModel: ObservableObject {
|
||||||
|
@Published var btnVM = ButtonViewModel()
|
||||||
|
|
||||||
|
@Published var titleName: String = ""
|
||||||
|
|
||||||
|
let leftBtnID = UUID()
|
||||||
|
let rightBtnID = UUID()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
|
||||||
|
btnVM.btnStates[leftBtnID] = ButtonState()
|
||||||
|
btnVM.btnStates[rightBtnID] = ButtonState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setLeftBtn(_ image: Image? = nil, text: String? = nil, font: Font? = nil, size: CGPoint, action: @escaping VOID_TO_VOID) {
|
||||||
|
btnVM.setSize(for: leftBtnID, newWidth: size.x, newHeight: size.y)
|
||||||
|
if text != nil {
|
||||||
|
btnVM.setText(for: leftBtnID, newText: text, newFont: font)
|
||||||
|
} else if let image = image {
|
||||||
|
btnVM.setImage(for: leftBtnID, newImage: image)
|
||||||
|
}
|
||||||
|
btnVM.setAction(for: leftBtnID, newAction: action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRightBtn(_ image: Image? = nil, text: String? = nil, font: Font? = nil, size: CGPoint, action: @escaping VOID_TO_VOID) {
|
||||||
|
btnVM.setSize(for: rightBtnID, newWidth: size.x, newHeight: size.y)
|
||||||
|
if text != nil {
|
||||||
|
btnVM.setText(for: rightBtnID, newText: text, newFont: font)
|
||||||
|
} else if let image = image {
|
||||||
|
btnVM.setImage(for: rightBtnID, newImage: image)
|
||||||
|
}
|
||||||
|
btnVM.setAction(for: rightBtnID, newAction: action)
|
||||||
|
}
|
||||||
|
// 여기에 프린트 문만 찍어줘
|
||||||
|
}
|
|
@ -1,37 +0,0 @@
|
||||||
//
|
|
||||||
// APIController.swift
|
|
||||||
// AcaMate
|
|
||||||
//
|
|
||||||
// Created by Sean Kim on 11/26/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
import Alamofire
|
|
||||||
|
|
||||||
public func loadAPIData<T: Decodable>(url: String, path: String,
|
|
||||||
method: HTTPMethod = .get,
|
|
||||||
parameters: [String: String],
|
|
||||||
headers: HTTPHeaders = [:],
|
|
||||||
decodingType: T.Type) -> Future<Any, Error> {
|
|
||||||
return Future { promise in
|
|
||||||
AF.request("\(url)\(path)",
|
|
||||||
method: method,
|
|
||||||
parameters: parameters,
|
|
||||||
headers: headers
|
|
||||||
)
|
|
||||||
.validate(statusCode: 200 ..< 300)
|
|
||||||
.responseString { response in
|
|
||||||
printLog(response)
|
|
||||||
}
|
|
||||||
.responseDecodable(of: decodingType) { response in
|
|
||||||
switch response.result {
|
|
||||||
case .success(let value):
|
|
||||||
promise(.success(value))
|
|
||||||
case .failure(let error):
|
|
||||||
promise(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,13 +11,12 @@ import KakaoSDKCommon
|
||||||
import KakaoSDKAuth
|
import KakaoSDKAuth
|
||||||
import KakaoSDKUser
|
import KakaoSDKUser
|
||||||
|
|
||||||
import Alamofire
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class LoginController {
|
class LoginController {
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
func login(_ type: SNSLoginType) -> AnyPublisher<SNSID,Error> {
|
func login(_ type: SNSLoginType, _ id: String = "") -> AnyPublisher<SNSID,Error> {
|
||||||
switch type {
|
switch type {
|
||||||
case .Kakao:
|
case .Kakao:
|
||||||
return self.checkKakaoToken()
|
return self.checkKakaoToken()
|
||||||
|
@ -29,14 +28,22 @@ class LoginController {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
//
|
|
||||||
|
|
||||||
case .Apple:
|
case .Apple:
|
||||||
return Fail(error: NSError(domain: "Apple login not implemented", code: 1, userInfo: nil))
|
return Fail(error: NSError(domain: "Apple login not implemented", code: 1, userInfo: nil))
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
case .Dev:
|
||||||
|
return Future<SNSID, Error> { promise in
|
||||||
|
var snsId = SNSID()
|
||||||
|
snsId.acctType = "dev"
|
||||||
|
snsId.snsId = "\(id)"
|
||||||
|
snsId.snsToken = "devToken"
|
||||||
|
promise(.success(snsId))
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func logout(type: SNSLoginType) {
|
func logout(type: SNSLoginType) {
|
||||||
|
@ -51,6 +58,7 @@ class LoginController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .Apple: break
|
case .Apple: break
|
||||||
|
case .Dev: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,7 +140,7 @@ extension LoginController {
|
||||||
if let sdkError = error as? SdkError, sdkError.isInvalidTokenError() == true {
|
if let sdkError = error as? SdkError, sdkError.isInvalidTokenError() == true {
|
||||||
// 로그인이 필요
|
// 로그인이 필요
|
||||||
self.loginKakao()
|
self.loginKakao()
|
||||||
// 로그인 후 동작이 sink에서 처리 될것
|
// 로그인 후 동작이 sink에서 처리 될것
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
|
134
AcaMate/5. Manager/APIManager.swift
Normal file
134
AcaMate/5. Manager/APIManager.swift
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
//
|
||||||
|
// APIController.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by Sean Kim on 11/26/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import Alamofire
|
||||||
|
|
||||||
|
public class APIManager {
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
// public static let shared = APIManager()
|
||||||
|
|
||||||
|
@UserDefault(key: "refresh", defaultValue: "refreshToken") var refresh
|
||||||
|
@UserDefault(key: "token", defaultValue: "accToken") var accToken
|
||||||
|
|
||||||
|
// private init(cancellables: Set<AnyCancellable> = Set<AnyCancellable>()) {
|
||||||
|
// self.cancellables = cancellables
|
||||||
|
// }
|
||||||
|
|
||||||
|
public func loadAPIData<T: APIResponseProtocol>(_ request: APIRequest<T>) -> Future<T, Error> {
|
||||||
|
let encoding: ParameterEncoding = (request.method == .get) ? URLEncoding.default : JSONEncoding.default
|
||||||
|
|
||||||
|
return Future { promise in
|
||||||
|
printLog(request.parameters)
|
||||||
|
AF.request("\(request.url)\(request.path)",
|
||||||
|
method: request.method,
|
||||||
|
parameters: request.parameters,
|
||||||
|
encoding: encoding,
|
||||||
|
headers: request.headers
|
||||||
|
)
|
||||||
|
.validate(statusCode: 200 ..< 300)
|
||||||
|
.responseDecodable(of: request.decoding) { response in
|
||||||
|
|
||||||
|
switch response.result {
|
||||||
|
case .success(let value):
|
||||||
|
printLog("Good: \(value)")
|
||||||
|
promise(.success(value))
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
printLog("Bad: \(error))")
|
||||||
|
promise(.failure(error))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func reloadAccessToken() -> Future<Any, Error> {
|
||||||
|
return Future { [weak self] promise in
|
||||||
|
guard let self = self else {return}
|
||||||
|
|
||||||
|
let request = APIRequest.init(path: "/api/v1/in/app/retryAccess",
|
||||||
|
parameters: ["refresh": refresh],
|
||||||
|
decoding: APIResponse<Access>.self)
|
||||||
|
|
||||||
|
APIManager().loadAPIData(request)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
promise(.failure(error))
|
||||||
|
printLog("\(error)")
|
||||||
|
case .finished:
|
||||||
|
printLog("엑세스 토큰 재발급 완료")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
guard let accToken = response as? APIResponse<Access> else {
|
||||||
|
promise(.failure(ACA_ERROR("Unknown ERROR")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch accToken.status.code {
|
||||||
|
case .success(let code):
|
||||||
|
if code == "000" {
|
||||||
|
if let tknData = accToken.data { promise(.success(tknData)) }
|
||||||
|
}
|
||||||
|
case .inputErr(let code):
|
||||||
|
// 로그인 화면으로 전환하기
|
||||||
|
promise(.failure(ACA_ERROR("Refresh token ERROR(\(code), \(accToken.status.message)")))
|
||||||
|
default:
|
||||||
|
// 그외에 서버에서 처리를 하다가 문제가 생겨서 발생하는 에러는 여기로 보낼거임
|
||||||
|
promise(.failure(ACA_ERROR("Unknown ERROR")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func loadUserAPIData<T: APIResponseProtocol & Codable>(request: APIRequest<T>) -> Future<T, Error>{
|
||||||
|
return Future { [weak self] promise in
|
||||||
|
guard let self = self else {return}
|
||||||
|
printLog(request.parameters["token"])
|
||||||
|
loadAPIData(request)
|
||||||
|
.flatMap { (response: T) -> AnyPublisher<T, Error> in
|
||||||
|
switch response.status.code {
|
||||||
|
case .inputErr(let code) where code == "101":
|
||||||
|
return self.reloadAccessToken()
|
||||||
|
.flatMap { response -> AnyPublisher<T, Error> in
|
||||||
|
self.accToken = (response as! Access).access
|
||||||
|
var updateRequest = request
|
||||||
|
updateRequest.parameters["token"] = self.accToken
|
||||||
|
printLog("Acc: \(self.accToken)")
|
||||||
|
return APIManager().loadAPIData(updateRequest)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
default:
|
||||||
|
return Just(response)
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
promise(.failure(error))
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { finalResponse in
|
||||||
|
promise(.success(finalResponse))
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
86
AcaMate/5. Manager/PermissionManager.swift
Normal file
86
AcaMate/5. Manager/PermissionManager.swift
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
//
|
||||||
|
// PermissionManager.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 3/28/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AVFoundation
|
||||||
|
import Photos
|
||||||
|
import CoreLocation
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
|
class PermissionManager: NSObject, ObservableObject {
|
||||||
|
|
||||||
|
private let locationManager = CLLocationManager()
|
||||||
|
private var locationRequestCallback: ((CLAuthorizationStatus) -> Void)?
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
locationManager.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 푸시 권한 요청
|
||||||
|
func requestPushPermission(completion: @escaping (Bool) -> Void) {
|
||||||
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(granted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 카메라 권한 요청
|
||||||
|
func requestCameraPermission(completion: @escaping (Bool) -> Void) {
|
||||||
|
AVCaptureDevice.requestAccess(for: .video) { granted in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(granted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 앨범 권한 요청
|
||||||
|
func requestPhotoPermission(completion: @escaping (PHAuthorizationStatus) -> Void) {
|
||||||
|
PHPhotoLibrary.requestAuthorization { status in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 위치 권한 요청
|
||||||
|
func requestLocationPermission(completion: @escaping (CLAuthorizationStatus) -> Void) {
|
||||||
|
locationRequestCallback = completion
|
||||||
|
locationManager.requestWhenInUseAuthorization()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 현재 권한 상태 확인
|
||||||
|
func checkPushPermission(completion: @escaping (Bool) -> Void) {
|
||||||
|
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(settings.authorizationStatus == .authorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCameraPermission() -> Bool {
|
||||||
|
return AVCaptureDevice.authorizationStatus(for: .video) == .authorized
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPhotoPermission() -> Bool {
|
||||||
|
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
||||||
|
return status == .authorized || status == .limited
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkLocationPermission() -> Bool {
|
||||||
|
let status = locationManager.authorizationStatus
|
||||||
|
return status == .authorizedAlways || status == .authorizedWhenInUse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extension PermissionManager: CLLocationManagerDelegate {
|
||||||
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||||
|
let status = manager.authorizationStatus
|
||||||
|
locationRequestCallback?(status)
|
||||||
|
locationRequestCallback = nil
|
||||||
|
}
|
||||||
|
}
|
119
AcaMate/5. Manager/WebSocketManager.swift
Normal file
119
AcaMate/5. Manager/WebSocketManager.swift
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
//
|
||||||
|
// WebSocketManager.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by TAnine on 2/17/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
import Starscream
|
||||||
|
|
||||||
|
// SignalR은 메세지의 끝이 \u{1E} 해당 유니코드 문자를 원함
|
||||||
|
class WebSocketManager: ObservableObject ,WebSocketDelegate {
|
||||||
|
|
||||||
|
@Published var socket: WebSocket?
|
||||||
|
@Published var receivedMessage: [String] = []
|
||||||
|
|
||||||
|
init() {
|
||||||
|
guard let url = URL(string: "\(WS_URL)/chatHub?transport=webSockets") else { return }
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.timeoutInterval = 5
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.setValue("Upgrade", forHTTPHeaderField: "Connection")
|
||||||
|
|
||||||
|
socket = WebSocket(request: request)
|
||||||
|
socket?.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect() {
|
||||||
|
printLog("TRY CONNECT SOCKET")
|
||||||
|
socket?.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func didReceive(event: Starscream.WebSocketEvent, client: WebSocketClient) {
|
||||||
|
switch event {
|
||||||
|
case .connected(let header):
|
||||||
|
printLog("CONNECTED : \(header)")
|
||||||
|
// 우리는 현재 JSON기반으로 메시지를 주고 받을거고 SignalR JSON 프로토콜의 버전은 1이다
|
||||||
|
let handShakeMsg =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"protocol":"json",
|
||||||
|
"version":1
|
||||||
|
}\u{1E}
|
||||||
|
"""
|
||||||
|
self.socket?.write(string: handShakeMsg)
|
||||||
|
|
||||||
|
case .disconnected(let reason, let code):
|
||||||
|
printLog("❌ DISCONNECTED: [\(code)] - \(reason)")
|
||||||
|
|
||||||
|
case .error(let error):
|
||||||
|
printLog("🆘 SOCKET ERROR: \(error?.localizedDescription ?? "unknown")")
|
||||||
|
|
||||||
|
case .text(let text):
|
||||||
|
// 서버에서 메세지를 보내면 여기로 들어올거고
|
||||||
|
// 받아온 텍스트의 앞은 target, [sendet, message] 순서로 들어온다.
|
||||||
|
printLog("📩 SERVER SENT: \(text)")
|
||||||
|
|
||||||
|
if let data = try? JSONSerialization.jsonObject(with: Data(text.utf8)) as? [String: Any],
|
||||||
|
let target = data["target"] as? String,
|
||||||
|
let args = data["arguments"] as? [String] {
|
||||||
|
|
||||||
|
if target == "ReceiveMessage" {
|
||||||
|
let sender = args[0]
|
||||||
|
let message = args[1]
|
||||||
|
receivedMessage.append(message)
|
||||||
|
print("💬 [\(sender)] \(message)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
case .binary(let binary):
|
||||||
|
printLog("BINARY?: \(binary)")
|
||||||
|
|
||||||
|
case .cancelled:
|
||||||
|
printLog("SOCKET CONNECTED CANCELLED!!!")
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func disconnect() {
|
||||||
|
socket?.disconnect()
|
||||||
|
socket?.delegate = nil // 추가: delegate 해제
|
||||||
|
socket = nil // 필요시, 소켓 객체 해제
|
||||||
|
printLog("CLOSE SOCKET")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMessage(_ message: String) {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"target":"SendMessage",
|
||||||
|
"arguments":["iOS", "\(message)"]
|
||||||
|
}\u{1E}
|
||||||
|
"""
|
||||||
|
socket?.write(string: json)
|
||||||
|
printLog("SEND THE MESSAGE: \(message)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinRoom(_ cid: String){
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"target":"JoinRoom",
|
||||||
|
"arguments":["\(cid)"]
|
||||||
|
}\u{1E}
|
||||||
|
"""
|
||||||
|
socket?.write(string: json)
|
||||||
|
printLog("JOIN: \(cid)")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +0,0 @@
|
||||||
//
|
|
||||||
// Text.swift
|
|
||||||
// AcaMate
|
|
||||||
//
|
|
||||||
// Created by Sean Kim on 12/1/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
|
|
||||||
extension Text {
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
//
|
|
||||||
// UIView.swift
|
|
||||||
// AcaMate
|
|
||||||
//
|
|
||||||
// Created by Sean Kim on 12/14/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
struct CustomTextField: UIViewRepresentable {
|
|
||||||
var placeholder: String
|
|
||||||
@Binding var text: String
|
|
||||||
|
|
||||||
var isSecure: Binding<Bool> = .constant(false)
|
|
||||||
|
|
||||||
var textColor = UIColor(Color(.Text.detail))
|
|
||||||
var font = UIFont(name: "NPS-font-Regular", size: 16)
|
|
||||||
|
|
||||||
// [필수] 초기화 시 UIView 생성
|
|
||||||
func makeUIView(context: Context) -> UITextField {
|
|
||||||
let txf = UITextField()
|
|
||||||
txf.placeholder = placeholder
|
|
||||||
txf.isSecureTextEntry = isSecure.wrappedValue
|
|
||||||
|
|
||||||
txf.textColor = textColor
|
|
||||||
txf.font = font
|
|
||||||
|
|
||||||
txf.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
let height = (font?.lineHeight ?? 10) + 8
|
|
||||||
txf.frame.size.height = height
|
|
||||||
|
|
||||||
txf.autocorrectionType = .no
|
|
||||||
txf.autocapitalizationType = .none
|
|
||||||
txf.smartInsertDeleteType = .no
|
|
||||||
txf.textContentType = .oneTimeCode
|
|
||||||
|
|
||||||
txf.delegate = context.coordinator
|
|
||||||
return txf
|
|
||||||
}
|
|
||||||
|
|
||||||
// [필수] 스유 상태 변화 따라 UIView 업데이트
|
|
||||||
func updateUIView(_ uiView: UITextField, context: Context) {
|
|
||||||
uiView.text = text
|
|
||||||
uiView.isSecureTextEntry = isSecure.wrappedValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
|
||||||
Coordinator(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
class Coordinator: NSObject, UITextFieldDelegate {
|
|
||||||
var parent: CustomTextField
|
|
||||||
|
|
||||||
init(_ parent: CustomTextField) {
|
|
||||||
self.parent = parent
|
|
||||||
}
|
|
||||||
|
|
||||||
func textFieldDidChangeSelection(_ textField: UITextField) {
|
|
||||||
parent.text = textField.text ?? ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
25
AcaMate/6. Modifier/Text.swift
Normal file
25
AcaMate/6. Modifier/Text.swift
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
// Text.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by Sean Kim on 12/1/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
protocol MultilineStyle: View {
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Text: MultilineStyle {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MultilineStyle {
|
||||||
|
func multilineStyle(_ alignment: TextAlignment = .leading, limit: Int = 1, scale: CGFloat = 0.5) -> some View {
|
||||||
|
return self
|
||||||
|
.lineLimit(limit)
|
||||||
|
.minimumScaleFactor(scale)
|
||||||
|
.multilineTextAlignment(alignment)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
}
|
||||||
|
}
|
150
AcaMate/6. Modifier/TextField.swift
Normal file
150
AcaMate/6. Modifier/TextField.swift
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
//
|
||||||
|
// UIView.swift
|
||||||
|
// AcaMate
|
||||||
|
//
|
||||||
|
// Created by Sean Kim on 12/14/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct FixedSizeWrapper<Content: UIView>: UIViewRepresentable {
|
||||||
|
let content: () -> Content
|
||||||
|
let width: CGFloat
|
||||||
|
let height: CGFloat
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> Content {
|
||||||
|
let view = content()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
view.widthAnchor.constraint(equalToConstant: width),
|
||||||
|
view.heightAnchor.constraint(equalToConstant: height)
|
||||||
|
])
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: Content, context: Context) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CustomTxfView: View {
|
||||||
|
|
||||||
|
var placeholder: String
|
||||||
|
@Binding var text: String
|
||||||
|
|
||||||
|
var maxLength: Int = 100
|
||||||
|
var isSecure: Binding<Bool> = .constant(false)
|
||||||
|
var alignment: TextAlignment = .leading
|
||||||
|
|
||||||
|
var textColor = Color(.Text.detail)
|
||||||
|
var font: Font = .nps(size:16)
|
||||||
|
// UIFont(name: "NPS-font-Regular", size: 16)
|
||||||
|
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
TextField(placeholder, text: $text)
|
||||||
|
// Binding<String>(
|
||||||
|
// get: { text },
|
||||||
|
// set: { newValue in
|
||||||
|
// text = String(newValue.prefix(maxLength))
|
||||||
|
// }
|
||||||
|
// ))
|
||||||
|
.font(font)
|
||||||
|
.tint(textColor)
|
||||||
|
.lineLimit(1)
|
||||||
|
.multilineTextAlignment(alignment)
|
||||||
|
.minimumScaleFactor(0.5)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
.clipped()
|
||||||
|
|
||||||
|
.onChange(of: text) { old, new in
|
||||||
|
if new.count > maxLength {
|
||||||
|
text = String(new.prefix(maxLength))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct CustomTextField: UIViewRepresentable {
|
||||||
|
var placeholder: String
|
||||||
|
@Binding var text: String
|
||||||
|
var maxLength: Int?
|
||||||
|
var isSecure: Binding<Bool> = .constant(false)
|
||||||
|
|
||||||
|
var textColor = UIColor(Color(.Text.detail))
|
||||||
|
var font = UIFont(name: "NPS-font-Regular", size: 16)
|
||||||
|
var alignment: NSTextAlignment = .left
|
||||||
|
|
||||||
|
// [필수] 초기화 시 UIView 생성
|
||||||
|
func makeUIView(context: Context) -> UITextField {
|
||||||
|
let txf = UITextField()
|
||||||
|
|
||||||
|
|
||||||
|
// txf.placeholder = placeholder
|
||||||
|
txf.attributedPlaceholder = NSAttributedString(
|
||||||
|
string: "\(placeholder)",
|
||||||
|
attributes: [NSAttributedString.Key.foregroundColor : UIColor(.Text.border)])
|
||||||
|
|
||||||
|
txf.isSecureTextEntry = isSecure.wrappedValue
|
||||||
|
|
||||||
|
txf.adjustsFontSizeToFitWidth = true
|
||||||
|
txf.minimumFontSize = 4
|
||||||
|
|
||||||
|
txf.textColor = textColor
|
||||||
|
let height = (font?.lineHeight ?? 10) + 8
|
||||||
|
txf.frame.size.height = height
|
||||||
|
|
||||||
|
txf.borderStyle = .none
|
||||||
|
txf.autocorrectionType = .no
|
||||||
|
txf.autocapitalizationType = .none
|
||||||
|
txf.smartInsertDeleteType = .no
|
||||||
|
txf.textContentType = .oneTimeCode
|
||||||
|
txf.textAlignment = self.alignment
|
||||||
|
txf.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
txf.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
txf.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
|
||||||
|
txf.delegate = context.coordinator
|
||||||
|
return txf
|
||||||
|
}
|
||||||
|
|
||||||
|
// [필수] 스유 상태 변화 따라 UIView 업데이트
|
||||||
|
func updateUIView(_ uiView: UITextField, context: Context) {
|
||||||
|
uiView.text = text
|
||||||
|
uiView.isSecureTextEntry = isSecure.wrappedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(self, max: maxLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UITextFieldDelegate {
|
||||||
|
var parent: CustomTextField
|
||||||
|
var maxLength: Int?
|
||||||
|
|
||||||
|
init(_ parent: CustomTextField, max: Int?) {
|
||||||
|
self.parent = parent
|
||||||
|
self.maxLength = max
|
||||||
|
}
|
||||||
|
|
||||||
|
func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||||
|
parent.text = textField.text ?? ""
|
||||||
|
}
|
||||||
|
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||||
|
let current = textField.text ?? ""
|
||||||
|
guard let stringRange = Range(range, in: current) else { return false}
|
||||||
|
let updated = current.replacingCharacters(in: stringRange, with: string)
|
||||||
|
return updated.count <= (maxLength ?? 9999)
|
||||||
|
}
|
||||||
|
|
||||||
|
// func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||||
|
// let currentText = textField.text ?? ""
|
||||||
|
// guard let stringRange = Range(range, in: currentText) else { return false }
|
||||||
|
// let updatedText = currentText.replacingCharacters(in: stringRange, with: string)
|
||||||
|
// return updatedText.count <= maxLength
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,13 +14,24 @@ struct NetworkModifier: ViewModifier {
|
||||||
@EnvironmentObject var appVM: AppViewModel
|
@EnvironmentObject var appVM: AppViewModel
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
if #available(iOS 17.0, *) {
|
||||||
.onChange(of: networkMonitor.isConnected) { _ , new in
|
content
|
||||||
if !new {
|
.onChange(of: networkMonitor.isConnected) { _ , new in
|
||||||
appVM.alertData = SetAlertData().setErrorNetwork()
|
if !new {
|
||||||
appVM.showAlert.toggle()
|
appVM.alertData = SetAlertData().setErrorNetwork()
|
||||||
|
appVM.showAlert.toggle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
|
||||||
|
content
|
||||||
|
.onChange(of: networkMonitor.isConnected) { new in
|
||||||
|
if !new {
|
||||||
|
appVM.alertData = SetAlertData().setErrorNetwork()
|
||||||
|
appVM.showAlert.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,16 +67,16 @@ struct LoadingModifier: ViewModifier {
|
||||||
.blur(radius: isLoading ? 3:0)
|
.blur(radius: isLoading ? 3:0)
|
||||||
if isLoading {
|
if isLoading {
|
||||||
Color.Text.detail.opacity(0.6)
|
Color.Text.detail.opacity(0.6)
|
||||||
// Color.Second.normal.opacity(0.3)
|
// Color.Second.normal.opacity(0.3)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
ProgressView("Loading...")
|
ProgressView("Loading...")
|
||||||
// .tint(Color.Text.black)
|
// .tint(Color.Text.black)
|
||||||
.tint(Color.Normal.normal)
|
.tint(Color.Normal.normal)
|
||||||
.scaleEffect(1.5)
|
.scaleEffect(1.5)
|
||||||
.foregroundStyle(Color.Normal.normal)
|
.foregroundStyle(Color.Normal.normal)
|
||||||
|
|
||||||
.font(.nps(font: .bold, size: 16))
|
.font(.nps(font: .bold, size: 16))
|
||||||
|
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,7 +135,7 @@ extension View {
|
||||||
}
|
}
|
||||||
|
|
||||||
func endTextEditing() {
|
func endTextEditing() {
|
||||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadingView(isLoading: Binding<Bool>) -> some View {
|
func loadingView(isLoading: Binding<Bool>) -> some View {
|
||||||
|
@ -140,13 +151,8 @@ extension View {
|
||||||
.mask(shape.fill(LinearGradient(gradient: Gradient(colors: [.black, .clear]), startPoint: .topLeading, endPoint: .bottomTrailing)))
|
.mask(shape.fill(LinearGradient(gradient: Gradient(colors: [.black, .clear]), startPoint: .topLeading, endPoint: .bottomTrailing)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// func pressAnimation(scale: CGFloat = 0.95, opacity: CGFloat = 0.85, duration: Double = 0.1) -> some View {
|
// func pressAnimation(scale: CGFloat = 0.95, opacity: CGFloat = 0.85, duration: Double = 0.1) -> some View {
|
||||||
// self.modifier(PressEffect(scale: scale, opacity: opacity, duration: duration))
|
// self.modifier(PressEffect(scale: scale, opacity: opacity, duration: duration))
|
||||||
// }
|
// }
|
||||||
}
|
|
||||||
extension View {
|
|
||||||
// func pressColorAnimation(backgroundColor: Color = Color.black.opacity(0.1), duration: Double = 0.1) -> some View {
|
|
||||||
// self.modifier(PressBackgroundEffect(backgroundColor: backgroundColor, duration: duration))
|
|
||||||
// }
|
|
||||||
}
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 811 KiB |
Binary file not shown.
Before Width: | Height: | Size: 811 KiB |
Binary file not shown.
Before Width: | Height: | Size: 811 KiB |
|
@ -29,7 +29,7 @@ struct AcaMateApp: App {
|
||||||
_ = AuthController.handleOpenUrl(url: url)
|
_ = AuthController.handleOpenUrl(url: url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.environmentObject(self.appVM)
|
.environmentObject(appVM)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,13 @@
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExceptionDomains</key>
|
<key>NSExceptionDomains</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>10.149.217.64</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSIncludesSubdomains</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>ipstein.myds.me</key>
|
<key>ipstein.myds.me</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
@ -43,5 +50,14 @@
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>external-accessory</string>
|
<string>external-accessory</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>카메라 접근이 필요합니다.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>사진 앨범 접근이 필요합니다.</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>앱 사용 중 위치 접근이 필요합니다.</string>
|
||||||
|
<key>NSUserTrackingUsageDescription</key>
|
||||||
|
<string>푸시 알림 권한을 요청합니다.</string>
|
||||||
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
</label>
|
</label>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Logo/App_Icon" translatesAutoresizingMaskIntoConstraints="NO" id="yVp-20-rR1">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Crystal_Icon.png" translatesAutoresizingMaskIntoConstraints="NO" id="yVp-20-rR1">
|
||||||
<rect key="frame" x="96.666666666666686" y="159" width="200" height="200"/>
|
<rect key="frame" x="96.666666666666686" y="159" width="200" height="200"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="200" id="USS-fT-KDG"/>
|
<constraint firstAttribute="height" constant="200" id="USS-fT-KDG"/>
|
||||||
|
@ -83,7 +83,7 @@
|
||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="Logo/App_Icon" width="1024" height="1024"/>
|
<image name="Crystal_Icon.png" width="1024" height="1024"/>
|
||||||
<image name="Logo/Team_Icon" width="144" height="144"/>
|
<image name="Logo/Team_Icon" width="144" height="144"/>
|
||||||
<namedColor name="Normal/Normal">
|
<namedColor name="Normal/Normal">
|
||||||
<color red="0.92199999094009399" green="0.875" blue="0.82400000095367432" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.92199999094009399" green="0.875" blue="0.82400000095367432" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
|
BIN
AcaMate/Resources/Assets.xcassets/AppIcon.appiconset/LOGO 1.png
Normal file
BIN
AcaMate/Resources/Assets.xcassets/AppIcon.appiconset/LOGO 1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 830 KiB |
BIN
AcaMate/Resources/Assets.xcassets/AppIcon.appiconset/LOGO 2.png
Normal file
BIN
AcaMate/Resources/Assets.xcassets/AppIcon.appiconset/LOGO 2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 830 KiB |
BIN
AcaMate/Resources/Assets.xcassets/AppIcon.appiconset/LOGO.png
Normal file
BIN
AcaMate/Resources/Assets.xcassets/AppIcon.appiconset/LOGO.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 830 KiB |
BIN
AcaMate/Resources/Assets.xcassets/Color Folder.zip
Normal file
BIN
AcaMate/Resources/Assets.xcassets/Color Folder.zip
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user