diff --git a/AcaMate.xcodeproj/project.xcworkspace/xcuserdata/tanine.xcuserdatad/UserInterfaceState.xcuserstate b/AcaMate.xcodeproj/project.xcworkspace/xcuserdata/tanine.xcuserdatad/UserInterfaceState.xcuserstate index 06a2d11..c26ba99 100644 Binary files a/AcaMate.xcodeproj/project.xcworkspace/xcuserdata/tanine.xcuserdatad/UserInterfaceState.xcuserstate and b/AcaMate.xcodeproj/project.xcworkspace/xcuserdata/tanine.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/AcaMate/0. Setup/WebView.swift b/AcaMate/0. Setup/WebView.swift index 5939531..3b8d13f 100644 --- a/AcaMate/0. Setup/WebView.swift +++ b/AcaMate/0. Setup/WebView.swift @@ -9,12 +9,51 @@ import SwiftUI import WebKit struct WebView: UIViewControllerRepresentable { - @Binding var isLoding: Bool + + + 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() + return WebViewController(url: url, isLoading: $isLoading, + complete: complete, coordinator: context.coordinator) + } @@ -22,24 +61,41 @@ struct WebView: UIViewControllerRepresentable { class WebViewController: UIViewController, WKUIDelegate { + var url: URL + @Binding var isLoading: Bool + var complete: ((Any) -> Void)? + var coordinator: WebView.Coordinator + + init(url: URL, isLoading: Binding, 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() } - func webView() { - let url = URL(string: "https://sean-59.github.io/Kakao-Postcode/")! + private func webView() { let request = URLRequest(url: url) let configuration = WKWebViewConfiguration() let contentController = WKUserContentController() - contentController.add(self, name: "callBackHandler") + contentController.add(coordinator, name: "callBackHandler") configuration.userContentController = contentController let webview = WKWebView(frame: view.bounds, configuration: configuration) webview.uiDelegate = self - webview.navigationDelegate = self + webview.navigationDelegate = coordinator webview.autoresizingMask = [.flexibleWidth, .flexibleHeight] webview.isUserInteractionEnabled = true @@ -51,28 +107,3 @@ class WebViewController: UIViewController, WKUIDelegate { } } - -extension WebViewController: WKNavigationDelegate { - func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { - print("웹뷰 로딩 시작") - } - - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - print("웹뷰 로딩 완료") - // 여기서 추가 작업(예: 로딩 인디케이터 숨기기)을 수행할 수 있습니다. - } -} - -extension WebViewController: WKScriptMessageHandler { - func userContentController(_ userContentController: WKUserContentController, - didReceive message: WKScriptMessage) { - if let data = message.body as? [String: Any] { - print(data) - print(data["jibunAddress"] ?? "jibunAddress 없음") - print(data["roadAddress"] ?? "roadAddress 없음") - print(data["zonecode"] ?? "zonecode 없음") - } - } -} - diff --git a/AcaMate/1. View/10. Common/DropDownView.swift b/AcaMate/1. View/10. Common/DropDownView.swift new file mode 100644 index 0000000..0b37b07 --- /dev/null +++ b/AcaMate/1. View/10. Common/DropDownView.swift @@ -0,0 +1,95 @@ +// +// DropDownView.swift +// AcaMate +// +// Created by TAnine on 3/25/25. +// + +import SwiftUI +class DropdownManager: ObservableObject { + static let shared = DropdownManager() + + @Published var isPresented = false + @Published var items: [String] = [] + @Published var anchor: CGRect = .zero + var onSelect: ((String) -> Void)? + + func show(items: [String], anchor: CGRect, onSelect: @escaping (String) -> Void) { + self.items = items + self.anchor = anchor + self.onSelect = onSelect + isPresented = true + } + + func dismiss() { + isPresented = false + } +} + +struct DropdownButton: View { + let title: String + let items: [String] + + @State private var frame: CGRect = .zero + + var body: some View { + Button { + DropdownManager.shared.show(items: items, anchor: frame) { selected in + print("선택한 항목:", selected) + } + } label: { + HStack { + Text(title) + Image(systemName: "chevron.down") + } + .padding() + .background(RoundedRectangle(cornerRadius: 8).stroke(Color.gray)) + } + .background( + GeometryReader { geo in + Color.clear + .onAppear { + frame = geo.frame(in: .global) + } + } + ) + } +} + + +struct GlobalDropdownOverlay: View { + @ObservedObject var manager = DropdownManager.shared + + var body: some View { + if manager.isPresented { + ZStack(alignment: .topLeading) { + Color.black.opacity(0.001) + .ignoresSafeArea() + .onTapGesture { + manager.dismiss() + } + + VStack(alignment: .leading, spacing: 0) { + ForEach(manager.items, id: \.self) { item in + Button { + manager.onSelect?(item) + manager.dismiss() + } label: { + Text(item) + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + } + .buttonStyle(.plain) + .background(Color.white) + } + } + .frame(width: manager.anchor.width) + .background(RoundedRectangle(cornerRadius: 8).stroke(Color.gray)) + .shadow(radius: 5) + .position(x: manager.anchor.midX, y: manager.anchor.maxY + 10) + } + } + } +} + + diff --git a/AcaMate/1. View/11. Intro & Login/RegisterView.swift b/AcaMate/1. View/11. Intro & Login/RegisterView.swift index 6c7d341..9f7ca23 100644 --- a/AcaMate/1. View/11. Intro & Login/RegisterView.swift +++ b/AcaMate/1. View/11. Intro & Login/RegisterView.swift @@ -24,134 +24,188 @@ struct RegisterView: View { @State private var scrollOffset: CGPoint = .zero @State private var showWebView = false + @State private var isLoading = false + @State private var isSelectAddr: Bool = false + private let addressBtnID = UUID() + private let registerBtnID = UUID() - let addressBtnID = UUID() - let registerBtnID = UUID() - + @State private var selected = "" + let options = ["Swift", "Kotlin", "Dart", "JavaScript"] var body: some View { // MARK: TO-DO // 회원가입 뷰 만들기 // 이름, 번호, 이메일, 주소, 생년월일 - VStack(spacing: 0) { - TopView(topVM: topVM) - - OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in + 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)) - // 이름 - HStack(spacing: 0){ - HStack(spacing: 0) { - Text("이름") - .font(.nps(size: 16)) - Text("*") - .font(.nps(size: 16)) - .foregroundStyle(Color(.Other.red)) + DropdownButton(title: "언어 선택", items: ["Swift", "Dart", "Kotlin"]) + + + 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) + Spacer(minLength: 1) + CustomTextField(placeholder: "최대 10글자 입력", text: $registerVM.nameText, alignment: .center) + .frame(maxWidth: .infinity,maxHeight: 48) + .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) + .background { + RoundedRectangle(cornerRadius: 24) + .foregroundStyle(Color(.Normal.light)) + } + } + .padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16)) + + // 생년월일 + HStack(spacing: 0){ + Text("생일") + .font(.nps(size: 16)) + .frame(width: 60, alignment: .center) + Spacer(minLength: 1) + DatePicker("", selection: $registerVM.selectDate, displayedComponents: [.date]) + .datePickerStyle(.compact) + .environment(\.locale, Locale(identifier: "ko_KR")) + .font(.nps(size: 16)) } - .frame(width: 60, alignment: .leading) - Spacer(minLength: 1) - CustomTextField(placeholder: "최대 10글자", text: $registerVM.nameText) - .frame(maxWidth: .infinity,maxHeight: 48) - .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) - .background { - RoundedRectangle(cornerRadius: 24) - .foregroundStyle(Color(.Normal.light)) - } - } - .padding() - - // 생년월일 - DatePicker("생일", selection: $registerVM.selectDate, displayedComponents: [.date]) - .datePickerStyle(.compact) - .environment(\.locale, Locale(identifier: "ko_KR")) - .font(.nps(size: 16)) .padding() - - // E-Mail - HStack(spacing: 0){ - Text("이메일") - .font(.nps(size: 16)) - .frame(width: 60, alignment: .leading) - Spacer(minLength: 1) - CustomTextField(placeholder: "앞부분 입력", text: $registerVM.emailFrontText) - .frame(maxWidth: .infinity,maxHeight: 48) - .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) - .background { - RoundedRectangle(cornerRadius: 24) - .foregroundStyle(Color(.Normal.light)) - } - // Spacer(minLength: 1) - Text("@") - .font(.nps(font: .bold, size: 16)) - .padding([.leading, .trailing], 4) - // Spacer(minLength: 1) - CustomTextField(placeholder: "뒷부분 입력", text: $registerVM.emailTailText) - .frame(maxWidth: .infinity,maxHeight: 48) - .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) - .background { - RoundedRectangle(cornerRadius: 24) - .foregroundStyle(Color(.Normal.light)) - } - } - .padding() - - // Phone - HStack(spacing: 0){ - Text("연락처") - .font(.nps(size: 16)) - .frame(width: 60, alignment: .leading) - CustomTextField(placeholder: "000", text: $registerVM.nameText) - .frame(maxWidth: .infinity,maxHeight: 48) - .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) - .background { - RoundedRectangle(cornerRadius: 24) - .foregroundStyle(Color(.Normal.light)) + // E-Mail + HStack(spacing: 0){ + HStack(spacing: 0) { + Text("이메일") + .font(.nps(size: 16)) + Text("*") + .font(.nps(size: 16)) + .foregroundStyle(Color(.Other.red)) } - Text("-") - .font(.nps(size: 16)) - .padding([.leading, .trailing], 4) - CustomTextField(placeholder: "0000", text: $registerVM.nameText) - .frame(maxWidth: .infinity,maxHeight: 48) - .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) - .background { - RoundedRectangle(cornerRadius: 24) - .foregroundStyle(Color(.Normal.light)) - } - Text("-") - .font(.nps(size: 16)) - .padding([.leading, .trailing], 4) - CustomTextField(placeholder: "0000", text: $registerVM.nameText) - .frame(maxWidth: .infinity,maxHeight: 48) - .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) - .background { - RoundedRectangle(cornerRadius: 24) - .foregroundStyle(Color(.Normal.light)) - } - } - .padding() - - HStack(spacing: 0){ - Text("연락처") - .font(.nps(size: 16)) - .frame(width: 60, alignment: .leading) - Spacer(minLength: 1) - SimpleBtnView(vm: btnVM, id: addressBtnID) - .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) + + Spacer(minLength: 1) + CustomTextField(placeholder: "앞부분 입력", text: $registerVM.emailFrontText, alignment: .center) + + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) + .background { + RoundedRectangle(cornerRadius: 24) + .foregroundStyle(Color(.Normal.light)) + } + // Spacer(minLength: 1) + Text("@") + .font(.nps(size: 16)) + .padding([.leading, .trailing], 4) + // Spacer(minLength: 1) + CustomTextField(placeholder: "뒷부분 입력", text: $registerVM.emailTailText, alignment: .center) + .frame(maxWidth: .infinity,maxHeight: 48) + .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) + .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)) + } + + CustomTextField(placeholder: "000", text: $registerVM.nameText, alignment: .center) + .frame(maxWidth: .infinity,maxHeight: 48) + .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) + .background { + RoundedRectangle(cornerRadius: 24) + .foregroundStyle(Color(.Normal.light)) + } + Text("-") + .font(.nps(size: 16)) + .padding([.leading, .trailing], 4) + CustomTextField(placeholder: "0000", text: $registerVM.nameText, alignment: .center) + .frame(maxWidth: .infinity,maxHeight: 48) + .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) + .background { + RoundedRectangle(cornerRadius: 24) + .foregroundStyle(Color(.Normal.light)) + } + Text("-") + .font(.nps(size: 16)) + .padding([.leading, .trailing], 4) + CustomTextField(placeholder: "0000", text: $registerVM.nameText, alignment: .center) + .frame(maxWidth: .infinity,maxHeight: 48) + .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) + .background { + RoundedRectangle(cornerRadius: 24) + .foregroundStyle(Color(.Normal.light)) + } + } + .padding() + + HStack(spacing: 0){ + Text("주소") + .font(.nps(size: 16)) + .frame(width: 60, alignment: .center) + Spacer(minLength: 1) + + VStack(spacing: 0) { + SimpleBtnView(vm: btnVM, id: addressBtnID) + .padding(.bottom, isSelectAddr ? 4:0) + CustomTextField(placeholder: "상세 주소 입력", text: $registerVM.addrDetailText, alignment: .center) + .frame(maxWidth: .infinity, + maxHeight: isSelectAddr ? 48 : 0) + .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) + .background { + RoundedRectangle(cornerRadius: 24) + .foregroundStyle(Color(.Normal.light)) + } + .opacity(isSelectAddr ? 1.0 : 0.0) + } + } + .padding() } - .padding() - - // address - + .frame(maxWidth: .infinity, maxHeight: .infinity) + // .background(Color(.Normal.light)) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .sheet(isPresented: $showWebView) { - WebView(isLoding: $showWebView) - .edgesIgnoringSafeArea(.all) + GlobalDropdownOverlay() + } + .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 +// printLog(registerVM.addressText) + registerVM.addressText = result.0 + + btnVM.setSize(for: addressBtnID, newWidth: .infinity, newHeight: .infinity) +// printLog(registerVM.addressText) + btnVM.setText(for: addressBtnID, newText: "\(registerVM.addressText)", newFont: .nps(size: 16)) + } } - } .onAppear { topVM.titleName = "회원가입" @@ -159,11 +213,12 @@ struct RegisterView: View { topVM.setRightBtn(size: CGPoint(x: 40, y: 40), action: rightAct) btnVM.setSize(for: addressBtnID, newWidth: 80, newHeight: 24) - btnVM.setText(for: addressBtnID, newText: "주소 입력", newFont: .nps(size: 16)) + btnVM.setText(for: addressBtnID, newText: "\(registerVM.addressText)", newFont: .nps(size: 16)) btnVM.setAction(for: addressBtnID) { -// self.appVM.isLoading.toggle() self.showWebView.toggle() } + } + .onChange(of: registerVM.addressText) { _, new in } } diff --git a/AcaMate/3. ViewModel/RegisterViewModel.swift b/AcaMate/3. ViewModel/RegisterViewModel.swift index fe64fe1..0fe8166 100644 --- a/AcaMate/3. ViewModel/RegisterViewModel.swift +++ b/AcaMate/3. ViewModel/RegisterViewModel.swift @@ -25,7 +25,8 @@ class RegisterViewModel: ObservableObject { @State var emailFrontText: String = "" @State var emailTailText: String = "" @State var phoneArray: [Int] = [] - @State var addressText: String = "" + @Published var addressText: String = "주소 입력" + @State var addrDetailText: String = "" } diff --git a/AcaMate/6. Modifier/TextField.swift b/AcaMate/6. Modifier/TextField.swift index abc178c..df72883 100644 --- a/AcaMate/6. Modifier/TextField.swift +++ b/AcaMate/6. Modifier/TextField.swift @@ -16,6 +16,7 @@ struct CustomTextField: UIViewRepresentable { 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 { @@ -37,6 +38,7 @@ struct CustomTextField: UIViewRepresentable { txf.autocapitalizationType = .none txf.smartInsertDeleteType = .no txf.textContentType = .oneTimeCode + txf.textAlignment = self.alignment txf.delegate = context.coordinator return txf