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 */; };
|
||||
A7A518CF2CF555E200822D0D /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = A7A518CE2CF555E200822D0D /* README.md */; };
|
||||
A7A518D12CF5588500822D0D /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = A7A518D02CF5588500822D0D /* .gitignore */; };
|
||||
FB0119D32D62EEF000C1FA82 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = FB0119D22D62EEF000C1FA82 /* Starscream */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
|
@ -53,6 +54,7 @@
|
|||
A73892252D526A9D00659A62 /* FirebaseCrashlytics in Frameworks */,
|
||||
A771FFF22CFB70D100367DA6 /* KakaoSDK in Frameworks */,
|
||||
A73892212D526A9D00659A62 /* FirebaseAnalytics in Frameworks */,
|
||||
FB0119D32D62EEF000C1FA82 /* Starscream in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -102,6 +104,7 @@
|
|||
A73892202D526A9D00659A62 /* FirebaseAnalytics */,
|
||||
A73892222D526A9D00659A62 /* FirebaseAppCheck */,
|
||||
A73892242D526A9D00659A62 /* FirebaseCrashlytics */,
|
||||
FB0119D22D62EEF000C1FA82 /* Starscream */,
|
||||
);
|
||||
productName = AcaMate;
|
||||
productReference = A7A518BB2CF5558B00822D0D /* AcaMate.app */;
|
||||
|
@ -135,6 +138,7 @@
|
|||
A78774702CF586AF002FE2EE /* XCRemoteSwiftPackageReference "Alamofire" */,
|
||||
A771FFF02CFB70D100367DA6 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */,
|
||||
A738921F2D526A9D00659A62 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
|
||||
FB0119D12D62EEF000C1FA82 /* XCRemoteSwiftPackageReference "Starscream" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = A7A518BC2CF5558B00822D0D /* Products */;
|
||||
|
@ -313,7 +317,7 @@
|
|||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -324,7 +328,7 @@
|
|||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = 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_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
|
@ -355,7 +359,7 @@
|
|||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -420,6 +424,14 @@
|
|||
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 */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
@ -448,6 +460,11 @@
|
|||
package = A78774702CF586AF002FE2EE /* XCRemoteSwiftPackageReference "Alamofire" */;
|
||||
productName = Alamofire;
|
||||
};
|
||||
FB0119D22D62EEF000C1FA82 /* Starscream */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = FB0119D12D62EEF000C1FA82 /* XCRemoteSwiftPackageReference "Starscream" */;
|
||||
productName = Starscream;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = A7A518B32CF5558B00822D0D /* Project object */;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"originHash" : "3b609245b8d633048f6670834279f82d0601cc0879a2d8c9c86fa0dd25734ea3",
|
||||
"originHash" : "2aab34be4ec6f8de8e42f37bee06d0f181ea2ab2db742d0c279bace3d5a0bcfb",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "abseil-cpp-binary",
|
||||
|
@ -127,6 +127,15 @@
|
|||
"version" : "2.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "starscream",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/daltoniam/Starscream",
|
||||
"state" : {
|
||||
"revision" : "c6bfd1af48efcc9a9ad203665db12375ba6b145a",
|
||||
"version" : "4.0.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-protobuf",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -17,9 +17,6 @@ import FirebaseCore
|
|||
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
printLog("Start Set AppDelegate")
|
||||
|
||||
|
@ -69,8 +66,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
|||
}
|
||||
|
||||
extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||
|
||||
//
|
||||
func registerForRemoteNotifications() {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
|
||||
print("Permission granted: \(granted)")
|
||||
|
@ -88,7 +83,9 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
|||
// 디바이스 토큰 등록 성공 시
|
||||
func application(_ application: UIApplication,didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
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 {
|
||||
let userInfo = notification.request.content.userInfo
|
||||
printLog(userInfo)
|
||||
|
||||
if let apsData = userInfo["aps"] as? [AnyHashable: Any],
|
||||
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, *) {
|
||||
return [[.list,.banner,.sound]]
|
||||
} else {
|
||||
|
|
|
@ -7,15 +7,41 @@
|
|||
import SwiftUI
|
||||
// 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
|
||||
//#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"
|
||||
//#else
|
||||
#if DEV
|
||||
//public let API_URL: String = "https://localhost:7086"
|
||||
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 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
|
||||
public let API_URL: String = "https://acamate.ipstein.myds.me"
|
||||
public let WS_URL: String = "wss://acamate.ipstein.myds.me"
|
||||
|
||||
#endif
|
||||
|
||||
public let API_HEADER = "iOS_AM_Connect_Key"
|
||||
|
||||
|
||||
// MARK: - TYPEALIAS
|
||||
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()
|
||||
} label: {
|
||||
content
|
||||
// if let image = image {
|
||||
// image
|
||||
// .resizable()
|
||||
// .frame(width: width, height: height)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,18 +35,17 @@ struct CircleBtnView: View {
|
|||
.foregroundStyle(state.foreColor)
|
||||
.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)")
|
||||
.font(font)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.5)
|
||||
.truncationMode(.tail)
|
||||
.foregroundStyle(state.foreColor)
|
||||
.multilineStyle()
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: state.width, height: state.height)
|
||||
.onTapGesture {
|
||||
endTextEditing()
|
||||
guard let action = state.action else {return}
|
||||
action()
|
||||
}
|
||||
|
|
|
@ -15,17 +15,15 @@ struct SimpleBtnView: View {
|
|||
|
||||
var body: some View {
|
||||
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)")
|
||||
.font(font)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.5)
|
||||
.multilineTextAlignment(.center)
|
||||
.truncationMode(.tail)
|
||||
.multilineStyle(.center)
|
||||
.foregroundStyle(state.textColor)
|
||||
.frame(width: state.width, height: state.height)
|
||||
.onTapGesture {
|
||||
if state.isUsable {
|
||||
endTextEditing()
|
||||
guard let action = state.action else { return }
|
||||
action()
|
||||
}
|
||||
|
@ -33,6 +31,7 @@ struct SimpleBtnView: View {
|
|||
}
|
||||
else {
|
||||
Button{
|
||||
endTextEditing()
|
||||
guard let action = state.action else { return }
|
||||
action()
|
||||
} 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 {
|
||||
@EnvironmentObject var appVM: AppViewModel
|
||||
@State private var naviState : NaviState = .init(act: .NONE, path: .Intro)
|
||||
@State private var history: [PathName] = [.Intro]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
|
||||
ZStack {
|
||||
switch naviState.path {
|
||||
switch appVM.naviState.path {
|
||||
case .NONE:
|
||||
EmptyView()
|
||||
case .Intro:
|
||||
IntroView(naviState: $naviState)
|
||||
IntroView(appVM)
|
||||
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:
|
||||
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 {
|
||||
case .NONE:
|
||||
break
|
||||
|
@ -54,6 +59,7 @@ struct NavigationView: View {
|
|||
.setAlert()
|
||||
.setNetwork()
|
||||
.loadingView(isLoading: $appVM.isLoading)
|
||||
|
||||
}
|
||||
|
||||
/// 경로에 한 단계 추가
|
||||
|
@ -64,7 +70,8 @@ struct NavigationView: View {
|
|||
/// 가장 가까운 경로 삭제
|
||||
private func popHistory() {
|
||||
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) {
|
||||
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) {
|
||||
let remove = history.count - history.firstIndex(of: path)! - 1
|
||||
history.removeLast(remove)
|
||||
if remove > 0 {
|
||||
naviState.set(act: .NONE, path: path)
|
||||
appVM.naviState.set(act: .NONE, path: path)
|
||||
// naviState.set(act: .NONE, path: path)
|
||||
return
|
||||
}
|
||||
}
|
||||
naviState.set(act: .RESET, path: path)
|
||||
appVM.naviState.set(act: .RESET, path: path)
|
||||
// naviState.set(act: .RESET, path: path)
|
||||
}
|
||||
|
||||
private func showHistory() {
|
||||
|
|
|
@ -1,117 +1,117 @@
|
|||
////
|
||||
//// AccountLoginView.swift
|
||||
//// AcaMate
|
||||
////
|
||||
//// Created by Sean Kim on 12/14/24.
|
||||
////
|
||||
//
|
||||
// AccountLoginView.swift
|
||||
// AcaMate
|
||||
//import SwiftUI
|
||||
//
|
||||
// Created by Sean Kim on 12/14/24.
|
||||
//struct AccountLoginView: View {
|
||||
// @ObservedObject var loginVM.: LoginViewModel
|
||||
// @Binding var userId: String
|
||||
// @Binding var password: String
|
||||
// @Binding var isSecure: Bool
|
||||
// @Binding var isSave: Bool
|
||||
// var body: some View {
|
||||
// VStack(spacing: 0) {
|
||||
// ZStack(alignment: .leading) {
|
||||
// if userId.isEmpty {
|
||||
// Text("아이디를 입력하세요.")
|
||||
// .font(.nps(font: .regular, size: 16))
|
||||
// .foregroundStyle(Color(.Text.border))
|
||||
// .padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||
// }
|
||||
// CustomTextField(placeholder: "", text: $userId)
|
||||
// .frame(maxWidth: .infinity,maxHeight: 24)
|
||||
// .padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||
// }
|
||||
// .background {
|
||||
// RoundedRectangle(cornerRadius: 24)
|
||||
// .foregroundStyle(.white)
|
||||
// }
|
||||
// .padding(EdgeInsets(top: 0, leading: 12, bottom: 8, trailing: 12))
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AccountLoginView: View {
|
||||
@ObservedObject var viewModel: LoginViewModel
|
||||
@Binding var userId: String
|
||||
@Binding var password: String
|
||||
@Binding var isSecure: Bool
|
||||
@Binding var isSave: Bool
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
ZStack(alignment: .leading) {
|
||||
if userId.isEmpty {
|
||||
Text("아이디를 입력하세요.")
|
||||
.font(.nps(font: .regular, size: 16))
|
||||
.foregroundStyle(Color(.Text.border))
|
||||
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||
}
|
||||
CustomTextField(placeholder: "", text: $userId)
|
||||
.frame(maxWidth: .infinity,maxHeight: 24)
|
||||
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||
}
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.padding(EdgeInsets(top: 0, leading: 12, bottom: 8, trailing: 12))
|
||||
|
||||
ZStack(alignment: .leading) {
|
||||
if password.isEmpty {
|
||||
Text("비밀번호를 입력하세요.")
|
||||
.font(.nps(font: .regular, size: 16))
|
||||
.foregroundStyle(Color(.Text.border))
|
||||
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||
}
|
||||
CustomTextField(placeholder: "", text: $password, isSecure: $isSecure)
|
||||
.frame(maxWidth: .infinity,maxHeight: 24)
|
||||
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Button {
|
||||
isSecure.toggle()
|
||||
} label: {
|
||||
if password.isEmpty {
|
||||
Rectangle()
|
||||
.frame(width: 16, height: 2)
|
||||
.foregroundStyle(Color(.Text.border))
|
||||
.padding(.trailing,24)
|
||||
}
|
||||
else {
|
||||
if isSecure {
|
||||
Image(systemName: "eye")
|
||||
.frame(width: 16, height: 16)
|
||||
.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))
|
||||
|
||||
Button {
|
||||
isSave.toggle()
|
||||
} label: {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
Spacer(minLength: 1)
|
||||
if isSave {
|
||||
Image(systemName: "checkmark.square")
|
||||
.foregroundStyle(Color(.Second.normal))
|
||||
.frame(width: 24, height: 24)
|
||||
} else {
|
||||
Image(systemName: "square")
|
||||
.foregroundStyle(Color(.Second.normal))
|
||||
.frame(width: 24, height: 24)
|
||||
}
|
||||
|
||||
Text("로그인 정보 저장")
|
||||
.font(.nps(font: .regular, size: 16))
|
||||
.foregroundStyle(Color(.Text.detail))
|
||||
}
|
||||
}
|
||||
.padding(EdgeInsets(top: 0, leading: 0, bottom: 24, trailing: 12))
|
||||
|
||||
Button {
|
||||
viewModel.loginAction.send(true)
|
||||
} label: {
|
||||
Text("로그인")
|
||||
.font(.nps(font: .bold, size: 24))
|
||||
.foregroundStyle(Color(.Text.white))
|
||||
.padding(EdgeInsets(top: 8, leading: 48, bottom: 8, trailing: 48))
|
||||
.background{
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.foregroundStyle(Color(.Second.normal))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ZStack(alignment: .leading) {
|
||||
// if password.isEmpty {
|
||||
// Text("비밀번호를 입력하세요.")
|
||||
// .font(.nps(font: .regular, size: 16))
|
||||
// .foregroundStyle(Color(.Text.border))
|
||||
// .padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||
// }
|
||||
// CustomTextField(placeholder: "", text: $password, isSecure: $isSecure)
|
||||
// .frame(maxWidth: .infinity,maxHeight: 24)
|
||||
// .padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||
//
|
||||
// HStack {
|
||||
// Spacer()
|
||||
// Button {
|
||||
// isSecure.toggle()
|
||||
// } label: {
|
||||
// if password.isEmpty {
|
||||
// Rectangle()
|
||||
// .frame(width: 16, height: 2)
|
||||
// .foregroundStyle(Color(.Text.border))
|
||||
// .padding(.trailing,24)
|
||||
// }
|
||||
// else {
|
||||
// if isSecure {
|
||||
// Image(systemName: "eye")
|
||||
// .frame(width: 16, height: 16)
|
||||
// .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))
|
||||
//
|
||||
// Button {
|
||||
// isSave.toggle()
|
||||
// } label: {
|
||||
// HStack(alignment: .center, spacing: 4) {
|
||||
// Spacer(minLength: 1)
|
||||
// if isSave {
|
||||
// Image(systemName: "checkmark.square")
|
||||
// .foregroundStyle(Color(.Second.normal))
|
||||
// .frame(width: 24, height: 24)
|
||||
// } else {
|
||||
// Image(systemName: "square")
|
||||
// .foregroundStyle(Color(.Second.normal))
|
||||
// .frame(width: 24, height: 24)
|
||||
// }
|
||||
//
|
||||
// Text("로그인 정보 저장")
|
||||
// .font(.nps(font: .regular, size: 16))
|
||||
// .foregroundStyle(Color(.Text.detail))
|
||||
// }
|
||||
// }
|
||||
// .padding(EdgeInsets(top: 0, leading: 0, bottom: 24, trailing: 12))
|
||||
//
|
||||
// Button {
|
||||
// loginVM..loginAction.send(true)
|
||||
// } label: {
|
||||
// Text("로그인")
|
||||
// .font(.nps(font: .bold, size: 24))
|
||||
// .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 Combine
|
||||
|
||||
|
||||
struct IntroView: View {
|
||||
@EnvironmentObject var appVM: AppViewModel
|
||||
|
||||
@StateObject var introVM: IntroViewModel
|
||||
@State var cancellables: Set<AnyCancellable> = []
|
||||
@Binding var naviState : NaviState
|
||||
|
||||
init(_ appVM: AppViewModel) {
|
||||
_introVM = StateObject(wrappedValue: IntroViewModel(appVM))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
.frame(height: 100)
|
||||
Image(.Logo.appIcon)
|
||||
Image(.Logo.crystalIcon)
|
||||
.resizable()
|
||||
.frame(width: 200, height: 200)
|
||||
Spacer()
|
||||
|
@ -39,92 +43,7 @@ struct IntroView: View {
|
|||
|
||||
.onAppear {
|
||||
printLog("IntroView_onAppear")
|
||||
#if LOCAL
|
||||
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)
|
||||
introVM.appStart()
|
||||
}
|
||||
}
|
||||
.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,24 +8,20 @@
|
|||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
|
||||
struct LoginView: View {
|
||||
@EnvironmentObject var appVM: AppViewModel
|
||||
@StateObject private var loginVM = LoginViewModel()
|
||||
@State var cancellables: Set<AnyCancellable> = []
|
||||
@Binding var naviState : NaviState
|
||||
@StateObject var loginVM: LoginViewModel
|
||||
|
||||
@State var selectIdLogin: Bool = false
|
||||
|
||||
@State var userId: String = ""
|
||||
@State var password: String = ""
|
||||
@State var isSecure: Bool = true
|
||||
@State var isSave: Bool = false
|
||||
init(_ appVM: AppViewModel) {
|
||||
_loginVM = StateObject(wrappedValue: LoginViewModel(appVM))
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Spacer().frame(height: 100)
|
||||
Image(.Logo.appIcon)
|
||||
Image(.Logo.crystalIcon)
|
||||
.resizable()
|
||||
.frame(width: 200, height: 200)
|
||||
// .padding(.top, 80)
|
||||
|
@ -35,83 +31,65 @@ struct LoginView: View {
|
|||
VStack(spacing: 16) {
|
||||
Button {
|
||||
// MARK: - TODO, 카카오 계정 로그인 구현
|
||||
appVM.isLoading.toggle()
|
||||
loginAction(type: .Kakao)
|
||||
// loginVM.toggleLoading = true
|
||||
loginVM.loginAction(type: .Kakao)
|
||||
|
||||
} label: {
|
||||
makeButton(image: Image(.Logo.kakaoIcon),color: Color(.Other.yellow), "카카오 계정으로 시작하기")
|
||||
}
|
||||
|
||||
Button {
|
||||
// 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: {
|
||||
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)
|
||||
|
||||
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)
|
||||
.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 {
|
||||
return HStack {
|
||||
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)
|
||||
HStack(alignment: .center, spacing: 2) {
|
||||
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))
|
||||
.frame(width: 28,alignment: .center)
|
||||
Text("/").font(.nps(size: 12))
|
||||
Text("/")
|
||||
.font(.nps(size: 12))
|
||||
.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))
|
||||
.frame(width: 28,alignment: .center)
|
||||
Text("\(cellText.group)")
|
||||
.font(.nps(size: 16))
|
||||
.foregroundStyle(Color(.Text.detail))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.5)
|
||||
.truncationMode(.tail)
|
||||
.multilineStyle()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
|
|
@ -48,9 +48,7 @@ struct CalCellView: View {
|
|||
Text("\(summaryCalData.summary)")
|
||||
.font(.nps(size: 20))
|
||||
.foregroundStyle(Color(.Text.black))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.5)
|
||||
.truncationMode(.tail)
|
||||
.multilineStyle()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,26 +8,39 @@
|
|||
import SwiftUI
|
||||
|
||||
struct HomeView: View {
|
||||
@StateObject private var topVM = TopViewModel()
|
||||
|
||||
@State private var scrollOffset: CGPoint = .zero
|
||||
@State private var topViewState: Bool = false
|
||||
|
||||
//MARK: - 변경 값
|
||||
@Binding var myType: UserType
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
ZStack {
|
||||
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
|
||||
VStack(spacing: 0) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
TopProfileView(userType: .Student)
|
||||
.padding(.bottom,12)
|
||||
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
|
||||
LazyVStack(spacing: 24) {
|
||||
|
||||
TopProfileView(myType: myType)
|
||||
|
||||
Group {
|
||||
AttendanceBoxView()
|
||||
|
||||
// CalendarBoxView(summaryCalDataList: [])
|
||||
CalendarBoxView(summaryCalDataList: [
|
||||
SummaryCalendar(id: "123", date: "2025-02-28", summary: "요약내용입니다."),
|
||||
SummaryCalendar(id: "123", date: "2025-02-28", summary: "요약내용입니다.")])
|
||||
|
||||
// ManagementBoxView(managementList: [])
|
||||
ManagementBoxView(managementList: [
|
||||
SummaryManagement(id: "01", title: "과목 명1", teacher: "A", ratio: 27, homework: 3),
|
||||
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),
|
||||
])
|
||||
|
||||
// NoticeBoxView(noticeList: [])
|
||||
NoticeBoxView(noticeList: [
|
||||
SummaryNotice(id: "00", title: "공지사항1", date: "2025-02-11", new: true),
|
||||
SummaryNotice(id: "01", title: "공지사항2", date: "2025-01-11", new: false),
|
||||
|
@ -56,14 +68,13 @@ struct HomeView: View {
|
|||
RoundedRectangle(cornerRadius: 8)
|
||||
.foregroundStyle(Color(.Other.cell))
|
||||
}
|
||||
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
|
||||
|
||||
.padding([.leading, .trailing], 24)
|
||||
}
|
||||
}
|
||||
|
||||
if topViewState {
|
||||
VStack(spacing: 0) {
|
||||
TopView(titleName: "Name")
|
||||
TopView(topVM: topVM)
|
||||
.transition(.move(edge: .top))
|
||||
.animation(.easeInOut, value: scrollOffset)
|
||||
Spacer(minLength: 1)
|
||||
|
@ -72,6 +83,20 @@ struct HomeView: View {
|
|||
}
|
||||
}
|
||||
.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
|
||||
if newValue > 200 && topViewState == false {
|
||||
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 {
|
||||
@StateObject var btnVM = ButtonViewModel()
|
||||
|
||||
var userType: UserType
|
||||
var myType: UserType
|
||||
|
||||
// MARK: TO-DO
|
||||
// 여기서 이름 떙겨오는것도 고민을 해야 함
|
||||
|
@ -48,27 +48,24 @@ struct TopProfileView: View {
|
|||
.padding([.top, .bottom], 40)
|
||||
VStack(alignment: .center, spacing: 8) {
|
||||
Text("\(self.academyName)")
|
||||
.frame(alignment: .center)
|
||||
.font(.nps(font: .bold, size: 36))
|
||||
.foregroundStyle(Color(.Text.title))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.5)
|
||||
.truncationMode(.tail)
|
||||
Text("\(self.myName)")
|
||||
.multilineTextAlignment(.center)
|
||||
.multilineStyle()
|
||||
.frame(alignment: .center)
|
||||
|
||||
Text("\(self.myName)")
|
||||
.font(.nps(size: 18))
|
||||
.foregroundStyle(Color(.Text.detail))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.5)
|
||||
.truncationMode(.tail)
|
||||
.multilineStyle()
|
||||
.frame(alignment: .center)
|
||||
|
||||
}
|
||||
} /// 위쪽 VStack
|
||||
.padding(EdgeInsets(top: 24, leading: 24, bottom: 12, trailing: 24))
|
||||
|
||||
// MARK: TO-DO
|
||||
// 여기에 가로스크롤 넣어야 할거 같음
|
||||
if userType == .Parent {
|
||||
if myType == .Parent {
|
||||
HStack(spacing: 0) {
|
||||
ForEach(Array(childIDList.enumerated()),id: \.offset){ index, id in
|
||||
CircleBtnView(vm: btnVM, id: id)
|
||||
|
@ -95,7 +92,7 @@ struct TopProfileView: View {
|
|||
// 마켓 버튼과 알림 버튼 동작 로직 구현하기
|
||||
|
||||
|
||||
switch self.userType {
|
||||
switch self.myType {
|
||||
case .Student:
|
||||
typeName = "학생"
|
||||
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
|
||||
|
||||
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 {
|
||||
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
|
||||
|
||||
struct CalendarView: View {
|
||||
@StateObject private var topVM = TopViewModel()
|
||||
|
||||
@State private var scrollOffset: CGPoint = .zero
|
||||
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
|
||||
|
||||
struct EtcView: View {
|
||||
@StateObject private var topVM = TopViewModel()
|
||||
@State private var scrollOffset: CGPoint = .zero
|
||||
|
||||
@Binding var myType: UserType
|
||||
|
||||
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 {
|
||||
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 {
|
||||
@EnvironmentObject var appVM: AppViewModel
|
||||
@EnvironmentObject var alertController: AlertController
|
||||
@State var cancellables: Set<AnyCancellable> = []
|
||||
@Binding var naviState : NaviState
|
||||
// @Binding var naviState : NaviState
|
||||
|
||||
@State private var myType: UserType = .Admin
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
@ -20,18 +21,18 @@ struct MainView: View {
|
|||
Group {
|
||||
switch appVM.menuName {
|
||||
case .Home:
|
||||
HomeView()
|
||||
HomeView(myType: $myType)
|
||||
case .Management:
|
||||
ManagementView()
|
||||
case .Chatting:
|
||||
ChattingView()
|
||||
ChattingView(appVM, $myType)
|
||||
case .Calendar:
|
||||
CalendarView()
|
||||
case .Etc:
|
||||
EtcView()
|
||||
EtcView(myType: $myType)
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 1)
|
||||
// Spacer(minLength: 1)
|
||||
|
||||
BottomView()
|
||||
.frame(maxWidth: .infinity)
|
||||
|
|
|
@ -8,37 +8,36 @@
|
|||
import SwiftUI
|
||||
|
||||
struct TopView: View {
|
||||
@StateObject var btnVM = ButtonViewModel()
|
||||
|
||||
@State var titleName: String = ""
|
||||
|
||||
@State private var leftBtnID = UUID()
|
||||
@State private var rightBtnID = UUID()
|
||||
|
||||
//MARK: - 변경 값
|
||||
var myType: UserType = .Student
|
||||
@ObservedObject var topVM: TopViewModel
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
SimpleBtnView(vm: btnVM, id: leftBtnID)
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
SimpleBtnView(vm: topVM.btnVM, id: topVM.leftBtnID)
|
||||
.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()
|
||||
.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))
|
||||
.font(.nps(font: .bold, size: 20))
|
||||
Spacer()
|
||||
|
||||
SimpleBtnView(vm: btnVM, id: rightBtnID)
|
||||
.padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 24))
|
||||
.frame(height: 40)
|
||||
.padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12))
|
||||
Spacer(minLength: 1)
|
||||
|
||||
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 {
|
||||
Rectangle()
|
||||
|
@ -47,48 +46,9 @@ struct TopView: View {
|
|||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.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
|
||||
|
||||
class APIResponse<T: Codable>: Codable {
|
||||
let status: Status
|
||||
let data: T
|
||||
public protocol APIResponseProtocol: Decodable {
|
||||
var status: Status { get }
|
||||
}
|
||||
|
||||
class Status: Codable {
|
||||
let code: String
|
||||
class APIResponse<T: Codable>: Codable, APIResponseProtocol {
|
||||
let status: Status
|
||||
let data: T?
|
||||
}
|
||||
|
||||
public class Status: Codable {
|
||||
let code: APICode
|
||||
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 {
|
||||
let os_type, final_ver, dev_ver, force_ver: String
|
||||
let choice_update_yn: Bool
|
||||
|
||||
}
|
||||
|
||||
// ----------------
|
||||
// /api/v1/in/app/retryAccess ----------------
|
||||
class Access: Codable {
|
||||
let access: String
|
||||
}
|
||||
|
||||
class User_Academy: Codable {
|
||||
let uid: String
|
||||
let bid: [String]
|
||||
// /api/v1/in/user ----------------
|
||||
class User: Codable {
|
||||
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 {
|
||||
return AlertData(title: "로그인",
|
||||
|
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
struct ButtonState {
|
||||
var image: Image? = nil
|
||||
|
||||
var title: String? = nil
|
||||
var text: String? = nil
|
||||
var font: Font? = nil
|
||||
|
||||
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 path: PathName
|
||||
|
||||
|
||||
|
||||
static func == (lhs: NaviState, rhs: NaviState) -> Bool {
|
||||
return lhs.act == rhs.act && lhs.path == rhs.path
|
||||
}
|
||||
|
@ -44,8 +42,10 @@ enum NaviAction: Hashable {
|
|||
enum PathName: Hashable {
|
||||
case Intro
|
||||
case Login
|
||||
case Register(_ type: SNSLoginType, id: String)
|
||||
case SelectAcademy
|
||||
case Main
|
||||
|
||||
case ChatRoom(id: String)
|
||||
case NONE
|
||||
}
|
||||
|
||||
|
|
|
@ -8,9 +8,10 @@
|
|||
import Foundation
|
||||
|
||||
|
||||
enum SNSLoginType{
|
||||
case Kakao
|
||||
case Apple
|
||||
enum SNSLoginType: String{
|
||||
case Apple = "ST00"
|
||||
case Kakao = "ST01"
|
||||
case Dev = "ST02"
|
||||
}
|
||||
|
||||
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 Combine
|
||||
//
|
||||
//import AVFoundation
|
||||
//import Photos
|
||||
//import CoreLocation
|
||||
//import UserNotifications
|
||||
|
||||
class AppViewModel: ObservableObject {
|
||||
// public static let shared = AppViewModel()
|
||||
|
||||
@Published var isLoading: Bool = false
|
||||
@Published var showAlert: Bool = false
|
||||
@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)
|
||||
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?) {
|
||||
var state = btnStates[id] ?? ButtonState()
|
||||
|
||||
state.title = newText
|
||||
state.text = newText
|
||||
state.font = newFont
|
||||
|
||||
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
|
||||
|
||||
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 KakaoSDKUser
|
||||
|
||||
import Alamofire
|
||||
import Foundation
|
||||
|
||||
class LoginController {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
func login(_ type: SNSLoginType) -> AnyPublisher<SNSID,Error> {
|
||||
func login(_ type: SNSLoginType, _ id: String = "") -> AnyPublisher<SNSID,Error> {
|
||||
switch type {
|
||||
case .Kakao:
|
||||
return self.checkKakaoToken()
|
||||
|
@ -29,13 +28,21 @@ class LoginController {
|
|||
}
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
//
|
||||
|
||||
case .Apple:
|
||||
return Fail(error: NSError(domain: "Apple login not implemented", code: 1, userInfo: nil))
|
||||
.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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -51,6 +58,7 @@ class LoginController {
|
|||
}
|
||||
}
|
||||
case .Apple: break
|
||||
case .Dev: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
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,6 +14,7 @@ struct NetworkModifier: ViewModifier {
|
|||
@EnvironmentObject var appVM: AppViewModel
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 17.0, *) {
|
||||
content
|
||||
.onChange(of: networkMonitor.isConnected) { _ , new in
|
||||
if !new {
|
||||
|
@ -21,6 +22,16 @@ struct NetworkModifier: ViewModifier {
|
|||
appVM.showAlert.toggle()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
content
|
||||
.onChange(of: networkMonitor.isConnected) { new in
|
||||
if !new {
|
||||
appVM.alertData = SetAlertData().setErrorNetwork()
|
||||
appVM.showAlert.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,8 +156,3 @@ extension View {
|
|||
// 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)
|
||||
}
|
||||
}
|
||||
.environmentObject(self.appVM)
|
||||
.environmentObject(appVM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,13 @@
|
|||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>10.149.217.64</key>
|
||||
<dict>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>ipstein.myds.me</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
|
@ -43,5 +50,14 @@
|
|||
<string>fetch</string>
|
||||
<string>external-accessory</string>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>카메라 접근이 필요합니다.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>사진 앨범 접근이 필요합니다.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>앱 사용 중 위치 접근이 필요합니다.</string>
|
||||
<key>NSUserTrackingUsageDescription</key>
|
||||
<string>푸시 알림 권한을 요청합니다.</string>
|
||||
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
</label>
|
||||
</subviews>
|
||||
</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"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="200" id="USS-fT-KDG"/>
|
||||
|
@ -83,7 +83,7 @@
|
|||
</scene>
|
||||
</scenes>
|
||||
<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"/>
|
||||
<namedColor name="Normal/Normal">
|
||||
<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