[] 학원 선택 페이지 작성 및 기존 코드 로직 분리 작업

This commit is contained in:
Seonkyu_Kim 2025-02-19 15:59:03 +09:00
parent ca49db680c
commit 71b2c3389f
11 changed files with 341 additions and 283 deletions

View File

@ -7,9 +7,12 @@
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
///
/// plist http
#if LOCAL
//public let API_URL: String = "http://0.0.0.0:5144"
public let API_URL: String = "https://localhost:7086"
public let API_URL: String = "http://10.149.217.64:5144"
//public let API_URL: String = "http://localhost:5144"
//public let API_URL: String = "https://localhost:7086"
public let WS_URL: String = "ws://localhost:5144"
/// URL

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

@ -11,16 +11,6 @@ import Combine
struct LoginView: View {
@EnvironmentObject var appVM: AppViewModel
@StateObject private var loginVM = LoginViewModel()
@State var cancellables: Set<AnyCancellable> = []
// @Binding var naviState : NaviState
@State var selectIdLogin: Bool = false
@State var userId: String = ""
@State var password: String = ""
@State var isSecure: Bool = true
@State var isSave: Bool = false
var body: some View {
VStack(spacing: 0) {
@ -35,15 +25,15 @@ 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,
// appVM.naviState.set(act: .MOVE, path: .Main)
appVM.naviState.set(act: .ADD, path: .SelectAcademy(bids: ["AA0000", "AA0001"]))
} label: {
@ -54,65 +44,17 @@ struct LoginView: View {
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)
.onChange(of: loginVM.navigateToAcademy, { _, _ in
appVM.naviState.set(act: .ADD, path: .SelectAcademy(bids: loginVM.bidArray))
})
.onChange(of: loginVM.toggleLoading) { _, new in
appVM.isLoading = new
}
}
func makeButton(image: Image, color: Color? = nil, _ body: String) -> some View {
@ -134,58 +76,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) {
// ID
appVM.naviState.set(act: .ADD, path: .SelectAcademy(bids: bidArray))
} else {
printLog("JSON 변환 실패")
}
}
}
.store(in: &cancellables)
}
}

View File

@ -6,56 +6,131 @@
//
import SwiftUI
import Combine
struct SelectAcademyView: View {
@State var cancellables: Set<AnyCancellable> = []
@EnvironmentObject var appVM: AppViewModel
@StateObject var saVM = SelectAcademyViewModel()
@State private var scrollOffset: CGPoint = .zero
var bids: [String]
@State private var academyCode: String = ""
var body: some View {
VStack(spacing: 0) {
Spacer()
.frame(height: 100)
.frame(maxHeight: 100)
Image(.Logo.appIcon)
.resizable()
.frame(width: 200, height: 200)
CustomTextField(placeholder: "학원 코드 입력", text: $academyCode)
VStack(spacing: 4) {
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: $saVM.academyCode)
.frame(maxWidth: .infinity,maxHeight: 48)
.padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
}
// .padding([.leading, .trailing], 24)
}
.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))
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
VStack(spacing: 12) {
ForEach(Array(saVM.academyList.enumerated()), id: \.offset) { index, academy in
AcademyCell(numbering: index, academy: saVM.academyList[index],selectNum: $saVM.selectNum){
saVM.toggleSelection(for: index)
}
}
}
.padding(EdgeInsets(top: 0, leading: 24, bottom: 12, trailing: 24))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
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(saVM.selectNum >= 0 ? 1 : 0)
.padding(EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24))
}
.onAppear {
loadAPIData(url: "\(API_URL)",
path: "/api/v1/in/member/academy",
method: .post,
parameters: ["bids": bids],
decodingType: APIResponse<[AcademyName]>.self)
.sink { completion in
switch completion {
case .failure(let error):
printLog("\(error)")
// appVM.isLoading.toggle()
case .finished:
break
// appVM.isLoading.toggle()
}
} receiveValue: { response in
guard let ua = response as? APIResponse<[AcademyName]> else {return}
printLog(ua)
}
.store(in: &cancellables)
saVM.loadAcademy(bids: bids)
}
}
}
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

@ -8,6 +8,49 @@
import SwiftUI
import Combine
class LoginViewModel: ObservableObject {
let loginAction = CurrentValueSubject<Bool, Never>(false)
private var cancellables = Set<AnyCancellable>()
@Published var toggleLoading: Bool = false
@Published var navigateToAcademy: Bool = false
@Published var bidArray: [String] = []
func loginAction(type: SNSLoginType) {
LoginController().login(type)
.flatMap{ snsId in
loadAPIData(url: "\(API_URL)",
path: "/api/v1/in/user/login",
parameters: [
"acctype": "\(type == .Apple ? "ST00": "ST01")",
"sns_id": "\(snsId.snsId)"
],
decodingType: APIResponse<User_Academy>.self)
}
.sink { [weak self] completion in
switch completion {
case .failure(let error):
printLog("\(error)")
self?.toggleLoading = false
case .finished:
self?.toggleLoading = false
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
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) {
// ID
printLog("GOOD")
self.navigateToAcademy = true
self.bidArray = bidArray
} else {
printLog("JSON 변환 실패")
}
}
}
.store(in: &cancellables)
}
}

View File

@ -0,0 +1,45 @@
//
// SelectAcademyViewModel.swift
// AcaMate
//
// Created by TAnine on 2/19/25.
//
import SwiftUI
import Combine
class SelectAcademyViewModel: ObservableObject {
private var cancellables: Set<AnyCancellable> = []
@Published var academyCode: String = ""
@Published var academyList: [AcademyName] = []
@Published var selectNum: Int = -1
func loadAcademy(bids: [String]) {
loadAPIData(url: "\(API_URL)",
path: "/api/v1/in/member/academy",
method: .post,
parameters: ["bids": bids],
decodingType: APIResponse<[AcademyName]>.self)
.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}
self.academyList = academyNames.data
}
.store(in: &cancellables)
}
func toggleSelection(for index: Int){
if selectNum == index {
selectNum = -1
} else {
selectNum = index
}}
}

View File

@ -11,7 +11,6 @@ import KakaoSDKCommon
import KakaoSDKAuth
import KakaoSDKUser
import Alamofire
import Foundation
class LoginController {
@ -29,7 +28,6 @@ class LoginController {
}
})
.eraseToAnyPublisher()
//
case .Apple:
return Fail(error: NSError(domain: "Apple login not implemented", code: 1, userInfo: nil))

View File

@ -16,12 +16,13 @@ public func loadAPIData<T: Decodable>(url: String, path: String,
parameters: [String: Any],
headers: HTTPHeaders = [:],//["Accept": "application/json"],
decodingType: T.Type) -> Future<Any, Error> {
let encoding: ParameterEncoding = (method == .get) ? URLEncoding.default : JSONEncoding.default
return Future { promise in
printLog(parameters)
AF.request("\(url)\(path)",
method: method,
parameters: parameters,
encoding: JSONEncoding.default,
encoding: encoding,
headers: headers
)
.validate(statusCode: 200 ..< 300)
@ -38,3 +39,5 @@ public func loadAPIData<T: Decodable>(url: String, path: String,
}
}
}

View File

@ -8,10 +8,6 @@
import SwiftUI
protocol MultilineStyle: View {
// func lineLimit(_ limit: Int?) -> Self
// func minimumScaleFactor(_ scale: CGFloat) -> Self
// func multilineTextAlignment(_ alignment: TextAlignment) -> Self
// func truncationMode(_ mode: Text.TruncationMode) -> Self
}
extension Text: MultilineStyle {

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>