Compare commits

...

31 Commits
main ... main

Author SHA1 Message Date
891fa0f016 [🎨] 로고 및 이미지 변경
모던 디자인 로고로 변경 및 웹 로고와 같은 아이콘으로 변경
2025-05-16 10:09:41 +09:00
77ee3083b3 [] 개발 계정 우회용 작업 UI 추가중 2025-04-11 17:48:50 +09:00
c2f540abd8 [] 개발자 우회 로그인 기능 추가 2025-04-10 18:00:48 +09:00
b226ad8050 [] SignalR 연결 - 채팅 관련 기능 추가 및 설정 중 2025-04-09 17:54:50 +09:00
46be2a0299 [🐛] 서버 연결 안될 시 처리하는 방법 수정 2025-04-02 17:22:18 +09:00
aa55b5463d [] 회원가입 및 로그인 로직 (카카오) 완성 2025-04-01 10:42:48 +09:00
519fcc537c [] 회원가입 로직 추가 생성 및 헤더 관련 로직 변경 추가 2025-03-28 17:56:33 +09:00
63031172a1 [] 드롭다운 최종 완성, 커스텀 텍스트 필드 전체 변경, 회원가입 로직 추가 중 2025-03-27 17:55:32 +09:00
4875427e24 [] 드롭다운 컴포넌트 생성 및 회원가입 화면에 적용중 2025-03-26 17:58:05 +09:00
f11d3b417a [] 회원가입 페이지 및 드롭다운 기능 추가중 2025-03-25 17:53:08 +09:00
5a2c237e3a [] 회원가입 화면 추가 및 몇가지 동작 로직 수정 2025-03-24 17:53:48 +09:00
45a28e386d [♻️] Intro 로직 변경 2025-03-24 09:03:27 +09:00
48c8f4dfda [♻️] Intro 관련 로직 수정 및 헤더 로직 추가 2025-03-20 17:45:12 +09:00
450240c364 [] header 정보 관련 새 로직 추가 2025-03-19 17:46:59 +09:00
966dd4cb34 [♻️] 엑세스 토큰으로 계정 정보 확인하는 로직 전체 변경 2025-03-18 14:59:25 +09:00
fea29d0cb8 [] 토큰 관련해서 테스트 및 기능 추가 중 2025-03-17 17:59:15 +09:00
195298dea1 [] 계정 토큰 관리 방법 변경에 따른 로직 추가 2025-03-14 17:37:31 +09:00
a825ff927e [] 로그인 동작 변경 및 회원가입 화면 작성 중 2025-03-12 17:28:53 +09:00
655144a2ee [] 푸시 수신 부 약간 변경 2025-03-06 17:56:31 +09:00
05f11df5e8 [♻️] API 받아오는 로직 수정 2025-02-26 17:25:32 +09:00
f4233f4562 [👷] JWT 인증 도입해 로그인 로직 변경 2025-02-25 17:32:32 +09:00
4b32aee2a4 [] 회원 가입뷰 및 회원 정보 확인 로직 추가 2025-02-20 17:56:54 +09:00
71b2c3389f [] 학원 선택 페이지 작성 및 기존 코드 로직 분리 작업 2025-02-19 15:59:03 +09:00
Seonkyu_Kim
ca49db680c [] 아카데미 인증 및 리스트 확인 뷰 작성 중 2025-02-18 17:33:05 +09:00
Seonkyu_Kim
9ae1fe4265 [] 아카데미 리스트 확인 페이지 작업 중 2025-02-18 17:32:16 +09:00
3a7d3cbd09 [👷🏻] 채팅 관련 수정 2025-02-18 01:23:06 +09:00
Seonkyu_Kim
9fb60762c4 [] 채팅(Base) 잡는 중: WebSocket 기반으로 구현 중 2025-02-17 17:50:28 +09:00
Seonkyu_Kim
875f92c81b [] 채팅 페이지 작업 중 2025-02-14 17:51:08 +09:00
Seonkyu_Kim
c6f224a9d9 [] 더보기 페이지 그리기 완료 2025-02-14 10:16:20 +09:00
Seonkyu_Kim
f75751670c [] 더보기 페이지 밑에 프로그램에 대한 정보 추가 중 2025-02-13 17:52:41 +09:00
Seonkyu_Kim
cb9e906d7c [] 더보기 페이지 그리기 완료 2025-02-13 16:05:37 +09:00
270 changed files with 3344 additions and 702 deletions

View File

@ -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 */;

View File

@ -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",

View File

@ -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 {

View File

@ -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 = () -> ()

View 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)
}
}

View File

@ -20,11 +20,6 @@ struct BoxBtnView<Content: View>: View {
action()
} label: {
content
// if let image = image {
// image
// .resizable()
// .frame(width: width, height: height)
// }
}
}
}

View File

@ -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()
}

View File

@ -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: {

View 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)
}
}
}
}
}

View File

@ -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() {

View File

@ -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))
// }
// }
// }
// }
//}

View File

@ -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())
//}

View File

@ -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()
//}

View 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)
}
}
}

View 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()
}
}
}

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -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))
}
}
}

View File

@ -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:

View File

@ -1,14 +0,0 @@
//
// ManagementView.swift
// AcaMate
//
// Created by TAnine on 2/11/25.
//
import SwiftUI
struct ManagementView: View {
var body: some View {
Text("학습 관리")
}
}

View 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))
}
}
}

View File

@ -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
//
}
}
}
}

View File

@ -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
}
}
}

View 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("오른쪽 버튼 클릭")
}
}

View File

@ -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("오른쪽 버튼 클릭")
}
}

View 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)
}
}
}

View 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("학원 정보 이동")
}
}
}
}
}

View 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)
}
}

View 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)
}
}

View File

@ -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("오른쪽 버튼 클릭")
}
}

View 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("개인정보 처리방침 이동")
}
}
}
}
}
//

View 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("알림 설정 페이지")
}
}
}
}

View 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("관리자 페이지 이동")
}
}
}
}
}
}

View File

@ -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()

View 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))
}
}
}

View File

@ -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)

View File

@ -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")
//}
}

View 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
}
}

View File

@ -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?
}

View File

@ -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: "로그인",

View File

@ -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

View 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
}

View 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
}
}

View File

@ -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
}

View File

@ -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 {

View 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?
}

View File

@ -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"
}

View File

@ -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)
}

View File

@ -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
// }
}

View File

@ -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

View 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
}
}

View 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}
}
}

View File

@ -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)
}
}

View 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)
}
}

View 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
}
}
}

View 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)
}
//
}

View File

@ -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))
}
}
}
}

View File

@ -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
}
}
}

View 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)
}
}
}

View 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
}
}

View 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)")
}
}

View File

@ -1,13 +0,0 @@
//
// Text.swift
// AcaMate
//
// Created by Sean Kim on 12/1/24.
//
import SwiftUI
extension Text {
}

View File

@ -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 ?? ""
}
}
}

View 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)
}
}

View 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
// }
}
}

View File

@ -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

View File

@ -29,7 +29,7 @@ struct AcaMateApp: App {
_ = AuthController.handleOpenUrl(url: url)
}
}
.environmentObject(self.appVM)
.environmentObject(appVM)
}
}
}

View File

@ -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>

View File

@ -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"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 KiB

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More