diff --git a/OpenMarket/OpenMarket.xcodeproj/project.pbxproj b/OpenMarket/OpenMarket.xcodeproj/project.pbxproj index 1598ae255..e10a100d9 100644 --- a/OpenMarket/OpenMarket.xcodeproj/project.pbxproj +++ b/OpenMarket/OpenMarket.xcodeproj/project.pbxproj @@ -17,14 +17,23 @@ 453EAC62287C347700EFCBB9 /* Currency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 453EAC4E287C050100EFCBB9 /* Currency.swift */; }; 453EAC64287C7BFB00EFCBB9 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 453EAC63287C7BFB00EFCBB9 /* URLSession.swift */; }; 453EAC67287C7C6300EFCBB9 /* URLCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 453EAC66287C7C6300EFCBB9 /* URLCommand.swift */; }; + 4573198D28910B5C00795ABB /* ProductDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4573198C28910B5C00795ABB /* ProductDetail.swift */; }; + 4573198F28910E3500795ABB /* ProductRegistration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4573198E28910E3500795ABB /* ProductRegistration.swift */; }; + 45A402472892746000B45A80 /* UITextField+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A402462892746000B45A80 /* UITextField+Extension.swift */; }; + 45A402492892B5A300B45A80 /* PickerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A402482892B5A300B45A80 /* PickerImageView.swift */; }; 45B7A50D287FF60500FCB69F /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B7A50C287FF60500FCB69F /* MainViewController.swift */; }; - AF2257722889391B00C5CE97 /* Int+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF2257712889391B00C5CE97 /* Int+Extensions.swift */; }; + AF2257722889391B00C5CE97 /* Double+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF2257712889391B00C5CE97 /* Double+Extensions.swift */; }; AF2257742889392300C5CE97 /* UILabel+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF2257732889392300C5CE97 /* UILabel+Extensions.swift */; }; AF2257762889393700C5CE97 /* UIImageView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF2257752889393700C5CE97 /* UIImageView+Extensions.swift */; }; AF24DE76288A86E700BF570B /* ImageCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF24DE75288A86E700BF570B /* ImageCacheManager.swift */; }; AF29C0382881016D00C719EE /* ListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF29C0372881016D00C719EE /* ListCell.swift */; }; AF29C03A2881034700C719EE /* GridCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF29C0392881034700C719EE /* GridCell.swift */; }; AF3D5330287C0FE500DBFAFA /* JsonDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF3D532F287C0FE500DBFAFA /* JsonDecoder.swift */; }; + AF6D741E289121C0007FC5A5 /* ModificationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF6D741D289121C0007FC5A5 /* ModificationData.swift */; }; + AFBC2FD52892636700E81A2F /* ProductSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFBC2FD42892636700E81A2F /* ProductSetupView.swift */; }; + AFBC2FD72892668800E81A2F /* ProductSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFBC2FD62892668800E81A2F /* ProductSetupViewController.swift */; }; + AFEB049D289933E10057B8DF /* ProductListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFEB049C289933E10057B8DF /* ProductListManager.swift */; }; + AFEB049F289936960057B8DF /* NotificationName+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFEB049E289936960057B8DF /* NotificationName+Extensions.swift */; }; AFFBDAA7287C0C1E001540FD /* ProductPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFFBDAA6287C0C1E001540FD /* ProductPage.swift */; }; C70FB0FB25BEF61C00C9924E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C70FB0FA25BEF61C00C9924E /* AppDelegate.swift */; }; C70FB0FD25BEF61C00C9924E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C70FB0FC25BEF61C00C9924E /* SceneDelegate.swift */; }; @@ -49,14 +58,23 @@ 453EAC57287C341400EFCBB9 /* DataFetchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataFetchTests.swift; sourceTree = ""; }; 453EAC63287C7BFB00EFCBB9 /* URLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; }; 453EAC66287C7C6300EFCBB9 /* URLCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLCommand.swift; sourceTree = ""; }; + 4573198C28910B5C00795ABB /* ProductDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetail.swift; sourceTree = ""; }; + 4573198E28910E3500795ABB /* ProductRegistration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductRegistration.swift; sourceTree = ""; }; + 45A402462892746000B45A80 /* UITextField+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Extension.swift"; sourceTree = ""; }; + 45A402482892B5A300B45A80 /* PickerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerImageView.swift; sourceTree = ""; }; 45B7A50C287FF60500FCB69F /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; - AF2257712889391B00C5CE97 /* Int+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Extensions.swift"; sourceTree = ""; }; + AF2257712889391B00C5CE97 /* Double+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extensions.swift"; sourceTree = ""; }; AF2257732889392300C5CE97 /* UILabel+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Extensions.swift"; sourceTree = ""; }; AF2257752889393700C5CE97 /* UIImageView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Extensions.swift"; sourceTree = ""; }; AF24DE75288A86E700BF570B /* ImageCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCacheManager.swift; sourceTree = ""; }; AF29C0372881016D00C719EE /* ListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCell.swift; sourceTree = ""; }; AF29C0392881034700C719EE /* GridCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridCell.swift; sourceTree = ""; }; AF3D532F287C0FE500DBFAFA /* JsonDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonDecoder.swift; sourceTree = ""; }; + AF6D741D289121C0007FC5A5 /* ModificationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModificationData.swift; sourceTree = ""; }; + AFBC2FD42892636700E81A2F /* ProductSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductSetupView.swift; sourceTree = ""; }; + AFBC2FD62892668800E81A2F /* ProductSetupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductSetupViewController.swift; sourceTree = ""; }; + AFEB049C289933E10057B8DF /* ProductListManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListManager.swift; sourceTree = ""; }; + AFEB049E289936960057B8DF /* NotificationName+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationName+Extensions.swift"; sourceTree = ""; }; AFFBDAA6287C0C1E001540FD /* ProductPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductPage.swift; sourceTree = ""; }; C70FB0F725BEF61C00C9924E /* OpenMarket.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenMarket.app; sourceTree = BUILT_PRODUCTS_DIR; }; C70FB0FA25BEF61C00C9924E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -90,6 +108,10 @@ AFFBDAA6287C0C1E001540FD /* ProductPage.swift */, 453EAC4C287C048300EFCBB9 /* Product.swift */, 453EAC4E287C050100EFCBB9 /* Currency.swift */, + 4573198C28910B5C00795ABB /* ProductDetail.swift */, + 4573198E28910E3500795ABB /* ProductRegistration.swift */, + AF6D741D289121C0007FC5A5 /* ModificationData.swift */, + AFEB049C289933E10057B8DF /* ProductListManager.swift */, ); path = Model; sourceTree = ""; @@ -110,12 +132,34 @@ path = Command; sourceTree = ""; }; + 4573199028912A8F00795ABB /* View */ = { + isa = PBXGroup; + children = ( + AFBC2FD42892636700E81A2F /* ProductSetupView.swift */, + AF29C0372881016D00C719EE /* ListCell.swift */, + AF29C0392881034700C719EE /* GridCell.swift */, + 45A402482892B5A300B45A80 /* PickerImageView.swift */, + ); + path = View; + sourceTree = ""; + }; + 4573199128912AA000795ABB /* Controller */ = { + isa = PBXGroup; + children = ( + 45B7A50C287FF60500FCB69F /* MainViewController.swift */, + AFBC2FD62892668800E81A2F /* ProductSetupViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; AF2257772889394100C5CE97 /* Extensions */ = { isa = PBXGroup; children = ( - AF2257712889391B00C5CE97 /* Int+Extensions.swift */, + AF2257712889391B00C5CE97 /* Double+Extensions.swift */, AF2257732889392300C5CE97 /* UILabel+Extensions.swift */, AF2257752889393700C5CE97 /* UIImageView+Extensions.swift */, + 45A402462892746000B45A80 /* UITextField+Extension.swift */, + AFEB049E289936960057B8DF /* NotificationName+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -151,15 +195,14 @@ C70FB0F925BEF61C00C9924E /* OpenMarket */ = { isa = PBXGroup; children = ( + 453EAC4B287C00C900EFCBB9 /* Model */, + 4573199128912AA000795ABB /* Controller */, + 4573199028912A8F00795ABB /* View */, + AF2257772889394100C5CE97 /* Extensions */, 453EAC65287C7C5900EFCBB9 /* Command */, AF3D532E287C0FD500DBFAFA /* Utility */, - 453EAC4B287C00C900EFCBB9 /* Model */, C70FB0FA25BEF61C00C9924E /* AppDelegate.swift */, C70FB0FC25BEF61C00C9924E /* SceneDelegate.swift */, - 45B7A50C287FF60500FCB69F /* MainViewController.swift */, - AF29C0372881016D00C719EE /* ListCell.swift */, - AF29C0392881034700C719EE /* GridCell.swift */, - AF2257772889394100C5CE97 /* Extensions */, C70FB10325BEF61D00C9924E /* Assets.xcassets */, C70FB10525BEF61D00C9924E /* LaunchScreen.storyboard */, C70FB10825BEF61D00C9924E /* Info.plist */, @@ -279,18 +322,27 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AFEB049D289933E10057B8DF /* ProductListManager.swift in Sources */, + AFEB049F289936960057B8DF /* NotificationName+Extensions.swift in Sources */, AF3D5330287C0FE500DBFAFA /* JsonDecoder.swift in Sources */, AFFBDAA7287C0C1E001540FD /* ProductPage.swift in Sources */, + 4573198D28910B5C00795ABB /* ProductDetail.swift in Sources */, AF24DE76288A86E700BF570B /* ImageCacheManager.swift in Sources */, AF29C0382881016D00C719EE /* ListCell.swift in Sources */, + 4573198F28910E3500795ABB /* ProductRegistration.swift in Sources */, + AF6D741E289121C0007FC5A5 /* ModificationData.swift in Sources */, AF2257742889392300C5CE97 /* UILabel+Extensions.swift in Sources */, AF29C03A2881034700C719EE /* GridCell.swift in Sources */, - AF2257722889391B00C5CE97 /* Int+Extensions.swift in Sources */, + 45A402472892746000B45A80 /* UITextField+Extension.swift in Sources */, + AF2257722889391B00C5CE97 /* Double+Extensions.swift in Sources */, + AFBC2FD52892636700E81A2F /* ProductSetupView.swift in Sources */, 453EAC64287C7BFB00EFCBB9 /* URLSession.swift in Sources */, 453EAC4F287C050100EFCBB9 /* Currency.swift in Sources */, AF2257762889393700C5CE97 /* UIImageView+Extensions.swift in Sources */, C70FB0FB25BEF61C00C9924E /* AppDelegate.swift in Sources */, C70FB0FD25BEF61C00C9924E /* SceneDelegate.swift in Sources */, + AFBC2FD72892668800E81A2F /* ProductSetupViewController.swift in Sources */, + 45A402492892B5A300B45A80 /* PickerImageView.swift in Sources */, 45B7A50D287FF60500FCB69F /* MainViewController.swift in Sources */, 453EAC67287C7C6300EFCBB9 /* URLCommand.swift in Sources */, 453EAC4D287C048300EFCBB9 /* Product.swift in Sources */, diff --git a/OpenMarket/OpenMarket.xcodeproj/xcshareddata/xcschemes/OpenMarket.xcscheme b/OpenMarket/OpenMarket.xcodeproj/xcshareddata/xcschemes/OpenMarket.xcscheme new file mode 100644 index 000000000..903a7faa8 --- /dev/null +++ b/OpenMarket/OpenMarket.xcodeproj/xcshareddata/xcschemes/OpenMarket.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OpenMarket/OpenMarket/Assets.xcassets/mara.imageset/Contents.json b/OpenMarket/OpenMarket/Assets.xcassets/mara.imageset/Contents.json new file mode 100644 index 000000000..ebf887e18 --- /dev/null +++ b/OpenMarket/OpenMarket/Assets.xcassets/mara.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "mara.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OpenMarket/OpenMarket/Assets.xcassets/mara.imageset/mara.png b/OpenMarket/OpenMarket/Assets.xcassets/mara.imageset/mara.png new file mode 100644 index 000000000..955c65aae Binary files /dev/null and b/OpenMarket/OpenMarket/Assets.xcassets/mara.imageset/mara.png differ diff --git a/OpenMarket/OpenMarket/Command/URLCommand.swift b/OpenMarket/OpenMarket/Command/URLCommand.swift index aab0ab9fe..e8473e843 100644 --- a/OpenMarket/OpenMarket/Command/URLCommand.swift +++ b/OpenMarket/OpenMarket/Command/URLCommand.swift @@ -5,8 +5,18 @@ // Created by 웡빙, 보리사랑 on 2022/07/12. // -enum URLData: String { - case host = "https://market-training.yagom-academy.kr" - case lookUpProductList = "/api/products?" +enum URLData { + static let host = "https://market-training.yagom-academy.kr" + static let apiPath = "/api/products" + static let identifier = "e4c0e472-0335-11ed-9676-05ce201d7309" + static let secret = "p0ilm9kwYb" +} + +enum HttpMethod: String { + case GET = "GET" + case POST = "POST" + case PATCH = "PATCH" + case DELETE = "DELETE" + case PUT = "PUT" } diff --git a/OpenMarket/OpenMarket/MainViewController.swift b/OpenMarket/OpenMarket/Controller/MainViewController.swift similarity index 79% rename from OpenMarket/OpenMarket/MainViewController.swift rename to OpenMarket/OpenMarket/Controller/MainViewController.swift index bf2dfc6ee..7f3b8d2bb 100644 --- a/OpenMarket/OpenMarket/MainViewController.swift +++ b/OpenMarket/OpenMarket/Controller/MainViewController.swift @@ -6,15 +6,16 @@ // import UIKit -class MainViewController: UIViewController { +final class MainViewController: UIViewController { // MARK: - Instance Properties - private let manager = NetworkManager() + private let manager = NetworkManager.shared private var collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout()) private var listDataSource: UICollectionViewDiffableDataSource? private var gridDataSource: UICollectionViewDiffableDataSource? private var listLayout: UICollectionViewLayout? = nil private var gridLayout: UICollectionViewLayout? = nil - + private var productListManager = ProductListManager() + private var currentMaximumPage = 1 enum Section { case main } @@ -61,10 +62,12 @@ class MainViewController: UIViewController { activityIndicator.stopAnimating() return activityIndicator }() - // MARK: - View Life Cycle override func viewDidLoad() { super.viewDidLoad() + NotificationCenter.default.addObserver(self, + selector: #selector(applyDataSource), + name: .addProductList, object: nil) initializeViewController() self.listLayout = createListLayout() self.gridLayout = createGridLayout() @@ -73,6 +76,10 @@ class MainViewController: UIViewController { configureListDataSource() configureGridDataSource() configureHierarchy() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) fetchData() } // MARK: - Main View Controller Method @@ -93,6 +100,9 @@ class MainViewController: UIViewController { @objc private func addButtonDidTapped() { print("add button tapped") + let prodcutDetailVC = ProductSetupViewController() + prodcutDetailVC.viewControllerTitle = "상품 등록" + navigationController?.pushViewController(prodcutDetailVC, animated: true) } private func setupSegment() { @@ -105,16 +115,26 @@ class MainViewController: UIViewController { } private func fetchData() { - manager.dataTask { [weak self] productList in - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - snapshot.appendItems(productList) - self?.gridDataSource?.apply(snapshot, animatingDifferences: false) - self?.listDataSource?.apply(snapshot, animatingDifferences: false) - DispatchQueue.main.async { - self?.activitiIndicator.stopAnimating() - self?.collectionView.alpha = 1 - } + manager.requestProductPage(at: 1) { [weak self] productList in + self?.productListManager.fetch(list: productList) + } + } + + private func loadData() { + manager.requestProductPage(at: currentMaximumPage) { [weak self] productList in + self?.productListManager.add(list: productList) + } + } + + @objc private func applyDataSource() { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(productListManager.productList) + self.gridDataSource?.apply(snapshot, animatingDifferences: false) + self.listDataSource?.apply(snapshot, animatingDifferences: false) + DispatchQueue.main.async { + self.activitiIndicator.stopAnimating() + self.collectionView.alpha = 1 } } } @@ -163,7 +183,6 @@ extension MainViewController { } listDataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView: UICollectionView, indexPath: IndexPath, identifier: Product) -> UICollectionViewCell? in - return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier) } } @@ -193,6 +212,11 @@ extension MainViewController { extension MainViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { collectionView.deselectItem(at: indexPath, animated: true) + let prodcutDetailVC = ProductSetupViewController() + prodcutDetailVC.productId = productListManager.productList[indexPath.row].id + prodcutDetailVC.viewControllerTitle = "상품 수정" + print("\(productListManager.productList[indexPath.row].id) - \(productListManager.productList[indexPath.row].name) is tapped") + navigationController?.pushViewController(prodcutDetailVC, animated: true) } } diff --git a/OpenMarket/OpenMarket/Controller/ProductSetupViewController.swift b/OpenMarket/OpenMarket/Controller/ProductSetupViewController.swift new file mode 100644 index 000000000..446e0db08 --- /dev/null +++ b/OpenMarket/OpenMarket/Controller/ProductSetupViewController.swift @@ -0,0 +1,217 @@ +// +// ProductSetupViewController.swift +// OpenMarket +// +// Created by 웡빙, 보리사랑 on 2022/07/28. +// + +import UIKit + +final class ProductSetupViewController: UIViewController { + // MARK: - Properties + private let manager = NetworkManager.shared + private var productSetupView: ProductSetupView? + private var imagePicker = UIImagePickerController() + var productId: Int? + var viewControllerTitle: String? + // MARK: - Life Cycle + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = .systemBackground + productSetupView = ProductSetupView(self) + setupNavigationItem() + setupKeyboard() + setupPickerViewController() + adoptTextFieldDelegate() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + guard let productId = productId else { + productSetupView?.horizontalStackView.addArrangedSubview(productSetupView?.addImageButton ?? UIButton()) + return + } + manager.requestProductDetail(at: productId) { detail in + DispatchQueue.main.async { [weak self] in + self?.updateSetup(with: detail) + } + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) + } + // MARK: - @objc method + @objc private func keyboardWillAppear(_ sender: Notification) { + guard let userInfo = sender.userInfo, let keyboarFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { + return + } + + let contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: keyboarFrame.size.height, right: 0.0) + productSetupView?.mainScrollView.contentInset = contentInset + productSetupView?.mainScrollView.scrollIndicatorInsets = contentInset + } + + @objc private func keyboardWillDisappear(_ sender: Notification) { + let contentInset = UIEdgeInsets.zero + productSetupView?.mainScrollView.contentInset = contentInset + productSetupView?.mainScrollView.scrollIndicatorInsets = contentInset + } + + @objc private func hideKeyboard(_ sender: Any) { + view.endEditing(true) + } + + @objc private func cancelButtonDidTapped() { + navigationController?.popViewController(animated: true) + } + + @objc private func doneButtonDidTapped() { + guard let productRegistration = createProductRegistration(), + let images = createImages() + else { + return + } + manager.requestProductRegistration(with: productRegistration, images: images) { detail in + print("SUCCESS POST - \(detail.id), \(detail.name)") + DispatchQueue.main.async { + self.showAlert(title: "알림", message: "게시 완료!!") { + self.navigationController?.popViewController(animated: true) + } + } + } + } + + @objc private func pickImage() { + if productSetupView?.horizontalStackView.subviews.count == 6 { + showAlert(title: "추가할 수 없습니다", message: "5장 이상은 추가 할 수 없습니다.") + return + } + self.present(imagePicker, animated: true) + } + + @objc private func changeCurrencyKeyboard() { + view.endEditing(true) + if productSetupView?.currencySegmentControl.selectedSegmentIndex == 0 { + productSetupView?.productPriceTextField.keyboardType = .numberPad + productSetupView?.productDiscountedPriceTextField.keyboardType = .numberPad + } else { + productSetupView?.productPriceTextField.keyboardType = .decimalPad + productSetupView?.productDiscountedPriceTextField.keyboardType = .decimalPad + } + } + // MARK: - ProductSetupVC - Private method + private func createProductRegistration() -> ProductRegistration? { + guard let productSetupView = productSetupView, + let productName = productSetupView.productNameTextField.text, + let price = Double(productSetupView.productPriceTextField.text ?? ""), + let discountedPrice = Double(productSetupView.productDiscountedPriceTextField.text ?? ""), + let stock = Int(productSetupView.productStockTextField.text ?? "") + else { + showAlert(title: "알림", message: "텍스트필드에 값을 넣어주세요") + return nil + } + let currency = productSetupView.currencySegmentControl.selectedSegmentIndex == 0 ? Currency.krw : Currency.usd + let productRegistration = ProductRegistration(name: productName, + descriptions: productSetupView.descriptionTextView.text, + price: price, + currency: currency, + discountedPrice: discountedPrice, + stock: stock, + secret: URLData.secret + ) + return productRegistration + } + + private func createImages() -> [UIImage]? { + guard let productSetupView = productSetupView else { + return nil + } + var subviews = productSetupView.horizontalStackView.subviews + subviews.removeFirst() + if subviews.count == 0 { + showAlert(title: "알림", message: "최소 한장의 이미지를 추가 해주세요.") + return nil + } + let images = subviews.map { (subview) -> UIImage in + let uiimage = subview as? UIImageView + return uiimage?.image ?? UIImage() + } + return images + } + + private func setupNavigationItem() { + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonDidTapped)) + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonDidTapped)) + navigationItem.title = self.viewControllerTitle + } + + private func setupKeyboard() { + productSetupView?.confirmButton.addTarget(self, action: #selector(hideKeyboard(_:)), for: .touchUpInside) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillAppear(_:)), name: UIResponder.keyboardWillShowNotification , object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillDisappear(_:)), name: UIResponder.keyboardWillHideNotification , object: nil) + productSetupView?.currencySegmentControl.addTarget(self, action: #selector(changeCurrencyKeyboard), for: .valueChanged) + } + + private func setupPickerViewController() { + self.imagePicker.sourceType = .photoLibrary + self.imagePicker.allowsEditing = true + self.imagePicker.delegate = self + productSetupView?.addImageButton.addTarget(self, action: #selector(pickImage), for: .touchUpInside) + } + + private func adoptTextFieldDelegate() { + productSetupView?.productNameTextField.delegate = self + productSetupView?.productPriceTextField.delegate = self + productSetupView?.productStockTextField.delegate = self + productSetupView?.productDiscountedPriceTextField.delegate = self + } + + private func updateSetup(with detail: ProductDetail) { + detail.images.forEach { image in + let imageView = PickerImageView(frame: CGRect()) + imageView.setImageUrl(image.url) + productSetupView?.horizontalStackView.addArrangedSubview(imageView) + } + productSetupView?.productNameTextField.text = detail.name + productSetupView?.productPriceTextField.text = String(detail.price) + productSetupView?.productDiscountedPriceTextField.text = String(detail.discountedPrice) + productSetupView?.productStockTextField.text = String(detail.stock) + productSetupView?.descriptionTextView.text = detail.description + productSetupView?.currencySegmentControl.selectedSegmentIndex = detail.currency == Currency.krw.rawValue ? 0 : 1 + } + + private func showAlert(title: String, message: String, _ completion: (() -> Void)? = nil) { + let failureAlert = UIAlertController(title: title, message: message, preferredStyle: .alert) + let confirmAction = UIAlertAction(title: "확인", style: .default) { _ in + guard let completion = completion else { return } + completion() + } + failureAlert.addAction(confirmAction) + present(failureAlert, animated: true) + } +} + +extension ProductSetupViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + var newImage: UIImage? = nil + if let possibleImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage { + newImage = possibleImage + } else if let possibleImge = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + newImage = possibleImge + } + let newImageView = PickerImageView(frame: CGRect()) + newImageView.image = newImage + productSetupView?.horizontalStackView.addArrangedSubview(newImageView) + picker.dismiss(animated: true) + } +} + +extension ProductSetupViewController: UITextFieldDelegate { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } +} diff --git a/OpenMarket/OpenMarket/Extensions/Int+Extensions.swift b/OpenMarket/OpenMarket/Extensions/Double+Extensions.swift similarity index 87% rename from OpenMarket/OpenMarket/Extensions/Int+Extensions.swift rename to OpenMarket/OpenMarket/Extensions/Double+Extensions.swift index 1f4243607..d2b525e07 100644 --- a/OpenMarket/OpenMarket/Extensions/Int+Extensions.swift +++ b/OpenMarket/OpenMarket/Extensions/Double+Extensions.swift @@ -1,5 +1,5 @@ // -// Int+Extensions.swift +// Double+Extensions.swift // OpenMarket // // Created by 웡빙, 보리사랑 on 2022/07/21. @@ -7,7 +7,7 @@ import Foundation -extension Int { +extension Double { func adoptDecimalStyle() -> String { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal diff --git a/OpenMarket/OpenMarket/Extensions/NotificationName+Extensions.swift b/OpenMarket/OpenMarket/Extensions/NotificationName+Extensions.swift new file mode 100644 index 000000000..2833219b0 --- /dev/null +++ b/OpenMarket/OpenMarket/Extensions/NotificationName+Extensions.swift @@ -0,0 +1,12 @@ +// +// NotificationName+Extensions.swift +// OpenMarket +// +// Created by 웡빙, 보리사랑 on 2022/08/02. +// + +import Foundation + +extension Notification.Name { + static let addProductList = Notification.Name("addProductList") +} diff --git a/OpenMarket/OpenMarket/Extensions/UIImageView+Extensions.swift b/OpenMarket/OpenMarket/Extensions/UIImageView+Extensions.swift index 309139c16..ff75fa278 100644 --- a/OpenMarket/OpenMarket/Extensions/UIImageView+Extensions.swift +++ b/OpenMarket/OpenMarket/Extensions/UIImageView+Extensions.swift @@ -43,4 +43,8 @@ extension UIImageView { }.resume() } } + func setFrame(at constant: Int) { + self.heightAnchor.constraint(equalToConstant: CGFloat(constant)).isActive = true + self.widthAnchor.constraint(equalToConstant: CGFloat(constant)).isActive = true + } } diff --git a/OpenMarket/OpenMarket/Extensions/UITextField+Extension.swift b/OpenMarket/OpenMarket/Extensions/UITextField+Extension.swift new file mode 100644 index 000000000..db1baac1b --- /dev/null +++ b/OpenMarket/OpenMarket/Extensions/UITextField+Extension.swift @@ -0,0 +1,22 @@ +// +// UITextField+Extension.swift +// OpenMarket +// +// Created by 웡빙, 보리사랑 on 2022/07/28. +// + +import Foundation +import UIKit + +extension UITextField { + func addLeftPadding() { + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: self.frame.height)) + self.leftView = paddingView + self.leftViewMode = ViewMode.always + } + func setupLayer() { + self.layer.borderColor = UIColor.lightGray.cgColor + self.layer.borderWidth = 1 + self.layer.cornerRadius = 2 + } +} diff --git a/OpenMarket/OpenMarket/Model/ModificationData.swift b/OpenMarket/OpenMarket/Model/ModificationData.swift new file mode 100644 index 000000000..aec93ba56 --- /dev/null +++ b/OpenMarket/OpenMarket/Model/ModificationData.swift @@ -0,0 +1,19 @@ +// +// ModificationRowData.swift +// OpenMarket +// +// Created by 웡빙, 보리사랑 on 2022/07/27. +// + +import Foundation + +struct ModificationData { + var id: Int + var name: String? = nil + var descriptions: String? = nil + var thumbnailId: String? = nil + var price: Double? = nil + var currency: Currency? = nil + var discountedPrice: Double? = nil + var stock: Int? = nil +} diff --git a/OpenMarket/OpenMarket/Model/Product.swift b/OpenMarket/OpenMarket/Model/Product.swift index 4e2eabe8b..c31effb5d 100644 --- a/OpenMarket/OpenMarket/Model/Product.swift +++ b/OpenMarket/OpenMarket/Model/Product.swift @@ -12,9 +12,9 @@ struct Product: Codable, Hashable { let name: String let thumbnail: String let currency: Currency - let price: Int - let bargainPrice: Int - let discountedPrice: Int + let price: Double + let bargainPrice: Double + let discountedPrice: Double let stock: Int let createdAt: String let issuedAt: String diff --git a/OpenMarket/OpenMarket/Model/ProductDetail.swift b/OpenMarket/OpenMarket/Model/ProductDetail.swift new file mode 100644 index 000000000..7a7ba3442 --- /dev/null +++ b/OpenMarket/OpenMarket/Model/ProductDetail.swift @@ -0,0 +1,71 @@ +// +// ProductDetail.swift +// OpenMarket +// +// Created by 웡빙, 보리사랑 on 2022/07/27. +// + +import Foundation + +// MARK: - ProductDetail +struct ProductDetail: Codable { + let id: Int + let vendorID: Int + let name: String + let description: String + let thumbnail: String + let currency: String + let price: Double + let bargainPrice: Double + let discountedPrice: Double + let stock: Int + let createdAt: String + let issuedAt: String + let images: [Image] + let vendors: Vendors + + enum CodingKeys: String, CodingKey { + case id + case vendorID = "vendor_id" + case name + case description + case thumbnail + case currency + case price + case bargainPrice = "bargain_price" + case discountedPrice = "discounted_price" + case stock + case createdAt = "created_at" + case issuedAt = "issued_at" + case images + case vendors + } +} + +// MARK: - Image +struct Image: Codable { + let id: Int + let url, thumbnailURL: String + let succeed: Bool + let issuedAt: String + + enum CodingKeys: String, CodingKey { + case id, url + case thumbnailURL = "thumbnail_url" + case succeed + case issuedAt = "issued_at" + } +} + +// MARK: - Vendors +struct Vendors: Codable { + let name: String + let id: Int + let createdAt, issuedAt: String + + enum CodingKeys: String, CodingKey { + case name, id + case createdAt = "created_at" + case issuedAt = "issued_at" + } +} diff --git a/OpenMarket/OpenMarket/Model/ProductListManager.swift b/OpenMarket/OpenMarket/Model/ProductListManager.swift new file mode 100644 index 000000000..b27855567 --- /dev/null +++ b/OpenMarket/OpenMarket/Model/ProductListManager.swift @@ -0,0 +1,28 @@ +// +// ProductListManager.swift +// OpenMarket +// +// Created by 웡빙, 보리사랑 on 2022/08/02. +// + +import Foundation + +class ProductListManager { + var productList: [Product] { + didSet { + NotificationCenter.default.post(name: .addProductList, object: nil) + } + } + + func fetch(list: [Product]) { + productList = list + } + + func add(list: [Product]) { + productList += list + } + + init() { + self.productList = [] + } +} diff --git a/OpenMarket/OpenMarket/Model/ProductRegistration.swift b/OpenMarket/OpenMarket/Model/ProductRegistration.swift new file mode 100644 index 000000000..d26cd0c99 --- /dev/null +++ b/OpenMarket/OpenMarket/Model/ProductRegistration.swift @@ -0,0 +1,30 @@ +// +// ProductRegistration.swift +// OpenMarket +// +// Created by 웡빙, 보리사랑 on 2022/07/27. +// + +import Foundation +import UIKit + +// MARK: - ProductRegistration +struct ProductRegistration: Codable { + let name: String + let descriptions: String + let price: Double + let currency: Currency + let discountedPrice: Double? + let stock: Int? + let secret: String + + enum CodingKeys: String, CodingKey { + case name + case descriptions + case price + case currency + case discountedPrice = "discounted_price" + case stock + case secret + } +} diff --git a/OpenMarket/OpenMarket/Utility/ImageCacheManager.swift b/OpenMarket/OpenMarket/Utility/ImageCacheManager.swift index 52ae37029..d0fe0a5dc 100644 --- a/OpenMarket/OpenMarket/Utility/ImageCacheManager.swift +++ b/OpenMarket/OpenMarket/Utility/ImageCacheManager.swift @@ -7,7 +7,7 @@ import UIKit -class ImageCacheManager { +final class ImageCacheManager { static let shared = NSCache() private init() {} } diff --git a/OpenMarket/OpenMarket/Utility/JsonDecoder.swift b/OpenMarket/OpenMarket/Utility/JsonDecoder.swift index 1e1ddd3f1..67db6ef37 100644 --- a/OpenMarket/OpenMarket/Utility/JsonDecoder.swift +++ b/OpenMarket/OpenMarket/Utility/JsonDecoder.swift @@ -7,6 +7,9 @@ import UIKit func decode(from data: Data, to type: T.Type) -> T? { + if type == String.self { + return String(data: data, encoding: .utf8) as? T + } let decoder = JSONDecoder() var fetchedData: T do { diff --git a/OpenMarket/OpenMarket/Utility/URLSession.swift b/OpenMarket/OpenMarket/Utility/URLSession.swift index 8f5405ca6..68dc7b596 100644 --- a/OpenMarket/OpenMarket/Utility/URLSession.swift +++ b/OpenMarket/OpenMarket/Utility/URLSession.swift @@ -6,35 +6,192 @@ // import Foundation +import UIKit -class NetworkManager { - func dataTask(_ completion: @escaping ([Product]) -> Void ) { +final class NetworkManager { + static let shared = NetworkManager() + private let session: URLSession + private init() { let config = URLSessionConfiguration.default - let session = URLSession(configuration: config) - var urlComponents = URLComponents(string: URLData.host.rawValue + URLData.lookUpProductList.rawValue) - let pageNo = URLQueryItem(name: "page_no", value: "1") + session = URLSession(configuration: config) + } + // MARK: - GET 상품 목록 조회 + func requestProductPage(at pageNumber: Int, _ completion: @escaping ([Product]) -> Void ) { + var urlComponents = URLComponents(string: URLData.host + URLData.apiPath + "?") + let pageNo = URLQueryItem(name: "page_no", value: String(pageNumber)) let itemsPerPage = URLQueryItem(name: "items_per_page", value: "20") urlComponents?.queryItems?.append(pageNo) urlComponents?.queryItems?.append(itemsPerPage) - guard let requestURL = urlComponents?.url else { + guard let url = urlComponents?.url else { + return + } + var request = URLRequest(url: url) + request.httpMethod = HttpMethod.GET.rawValue + let dataTask = createDataTask(request: request, type: ProductPage.self) { productPage in + completion(productPage.pages) + } + dataTask.resume() + } + // MARK: - GET 상품 상세 조회 + func requestProductDetail(at id: Int, _ completion: @escaping (ProductDetail) -> Void ) { + guard let url = URLComponents(string: URLData.host + URLData.apiPath + "/\(id)")?.url else { + return + } + var request = URLRequest(url: url) + request.httpMethod = HttpMethod.GET.rawValue + let dataTask = createDataTask(request: request, type: ProductDetail.self) { detail in + completion(detail) + } + dataTask.resume() + } + // MARK: - POST 상품 등록 + func requestProductRegistration(with registration: ProductRegistration, images: [UIImage] ,_ completion: @escaping (ProductDetail) -> Void) { + guard let url = URL(string: URLData.host + URLData.apiPath) else { + return + } + let boundary = UUID().uuidString + var request = URLRequest(url: url) + request.httpMethod = HttpMethod.POST.rawValue + request.setValue(URLData.identifier, forHTTPHeaderField: "identifier") + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + request.httpBody = createPostBody(with: registration, images: images, at: boundary) + let dataTask = createDataTask(request: request, type: ProductDetail.self) { detail in + completion(detail) + } + dataTask.resume() + } + // MARK: - FETCH 상품 수정 + func requestProductModification(id: Int, rowData: String, _ completion: @escaping (ProductDetail) -> Void) { + guard let url = URL(string: URLData.host + URLData.apiPath + "/\(id)") else { + return + } + let parameters = rowData + let postData = parameters.data(using: .utf8) + var request = URLRequest(url: url) + request.httpMethod = "PATCH" + request.httpBody = postData + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(URLData.identifier, forHTTPHeaderField: "identifier") + let dataTask = createDataTask(request: request, type: ProductDetail.self) { detail in + completion(detail) + } + dataTask.resume() + } + // MARK: - DELETE 상품 삭제 + func requestProductDeleteKey(id: Int, _ completion: @escaping (String) -> Void) { + guard let url = URL(string: URLData.host + URLData.apiPath + "/\(id)/secret") else { + return + } + var request = URLRequest(url: url) + request.httpMethod = HttpMethod.POST.rawValue + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(URLData.identifier, forHTTPHeaderField: "identifier") + request.httpBody = "{\"secret\": \"\(URLData.secret)\"}".data(using: .utf8) + let dataTask = createDataTask(request: request, type: String.self) { deleteKey in + completion(deleteKey) + } + dataTask.resume() + } + + func requestProductDelete(id: Int, key: String) { + guard let url = URL(string: URLData.host + URLData.apiPath + "/\(id)/\(key)") else { return } - let dataTask = session.dataTask(with: requestURL) { (data, response, error) in + var request = URLRequest(url: url) + request.httpMethod = HttpMethod.DELETE.rawValue + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(URLData.identifier, forHTTPHeaderField: "identifier") + let dataTask = createDataTask(request: request, type: ProductDetail.self) { detail in + print("COMPL") + } + dataTask.resume() + } +} + +extension NetworkManager { + private func createDataTask(request: URLRequest, type: T.Type, _ completion: @escaping (T) -> Void ) -> URLSessionDataTask { + session.dataTask(with: request) { (data, response, error) in guard error == nil else { return } let successsRange = 200..<300 guard let statusCode = (response as? HTTPURLResponse)?.statusCode, successsRange.contains(statusCode) else { - return - } + return + } guard let resultData = data, - let fetchedData = decode(from: resultData, to: ProductPage.self) else { - debugPrint("ERROR: FAILURE DECODING ") - return + let fetchedData = decode(from: resultData, to: type.self) else { + debugPrint("ERROR: FAILURE DECODING ") + return + } + completion(fetchedData) + } + } + + private func createPostBody(with inputData: ProductRegistration, images: [UIImage], at boundary: String) -> Data? { + var data = Data() + guard let paramData = try? JSONEncoder().encode(inputData) else { + return nil + } + guard let startBoundaryData = "\r\n--\(boundary)\r\n".data(using: .utf8) else { + return nil + } + data.append(startBoundaryData) + guard let paramsAttribute = "Content-Disposition: form-data; name=\"params\"\r\n\r\n".data(using: .utf8) else { + return nil + } + data.append(paramsAttribute) + data.append(paramData) + for (index, image) in images.enumerated() { + data.append(startBoundaryData) + let fileName = "\(inputData.name) - \(index)" + data.append("Content-Disposition: form-data; name=\"images\"; filename=\"\(fileName).png\"\r\n".data(using: .utf8)!) + data.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!) + guard let compressionImage = compress(image) else { + return nil } - completion(fetchedData.pages) + data.append(compressionImage) } - dataTask.resume() + guard let endBoundaryData = "\r\n--\(boundary)--\r\n".data(using: .utf8) else { + return nil + } + data.append(endBoundaryData) + return data + } + + func translateToRowData(_ data: ModificationData) -> String { + var detailToModify = ["secret": nil, "name": nil, "descriptions": nil, + "thumbnail_id": nil, "price": nil, "currency": nil, + "discounted_price": nil, "stock": nil] as [String : Any?] + detailToModify["secret"] = URLData.secret + detailToModify["name"] = data.name + detailToModify["descriptions"] = data.descriptions + detailToModify["thumbnail_id"] = data.thumbnailId + detailToModify["price"] = data.price + detailToModify["currency"] = data.currency?.rawValue + detailToModify["discounted_price"] = data.discountedPrice + detailToModify["stock"] = data.stock + + var result: [String] = [] + for (key, value) in detailToModify { + if value != nil { + if value is String || value is Currency { + result.append("\"\(key)\": \"\(value!)\"") + } else { + result.append("\"\(key)\": \(value!)") + } + } + } + return "{\(result.joined(separator: ","))}" + } + private func compress(_ Image: UIImage) -> Data? { + guard var compressedImage = Image.jpegData(compressionQuality: 0.2) else { + return nil + } + while compressedImage.count > 307200 { + compressedImage = UIImage(data: compressedImage)?.jpegData(compressionQuality: 0.5) ?? Data() + } + return compressedImage } } + diff --git a/OpenMarket/OpenMarket/GridCell.swift b/OpenMarket/OpenMarket/View/GridCell.swift similarity index 93% rename from OpenMarket/OpenMarket/GridCell.swift rename to OpenMarket/OpenMarket/View/GridCell.swift index cfdeb1276..04a66a616 100644 --- a/OpenMarket/OpenMarket/GridCell.swift +++ b/OpenMarket/OpenMarket/View/GridCell.swift @@ -87,10 +87,7 @@ final class GridCell: UICollectionViewCell { private func setupConstraints() { NSLayoutConstraint.activate([ - productImageView.heightAnchor.constraint(equalToConstant: 130), - productImageView.widthAnchor.constraint(equalToConstant: 130), - productImageView.leadingAnchor.constraint(equalTo: self.verticalStackView.leadingAnchor, constant: 10), - productImageView.trailingAnchor.constraint(equalTo: self.verticalStackView.trailingAnchor, constant: -10), + productImageView.heightAnchor.constraint(equalTo:self.contentView.heightAnchor, multiplier: 0.5), productImageView.heightAnchor.constraint(equalTo: self.productImageView.widthAnchor) ]) NSLayoutConstraint.activate([ diff --git a/OpenMarket/OpenMarket/ListCell.swift b/OpenMarket/OpenMarket/View/ListCell.swift similarity index 89% rename from OpenMarket/OpenMarket/ListCell.swift rename to OpenMarket/OpenMarket/View/ListCell.swift index 8a07f3889..9c60a286d 100644 --- a/OpenMarket/OpenMarket/ListCell.swift +++ b/OpenMarket/OpenMarket/View/ListCell.swift @@ -20,7 +20,6 @@ final class ListCell: UICollectionViewCell { let stackview = UIStackView() stackview.translatesAutoresizingMaskIntoConstraints = false stackview.axis = .vertical - stackview.alignment = .fill stackview.distribution = .fillEqually return stackview }() @@ -40,6 +39,7 @@ final class ListCell: UICollectionViewCell { label.font = UIFont.preferredFont(forTextStyle: .title2) label.numberOfLines = 0 label.text = "Mac mini" + label.setContentHuggingPriority(UILayoutPriority(251), for: .horizontal) return label }() @@ -68,6 +68,7 @@ final class ListCell: UICollectionViewCell { label.textColor = .lightGray label.font = UIFont.preferredFont(forTextStyle: .body) label.text = "JPY 800" + label.setContentHuggingPriority(UILayoutPriority(251), for: .horizontal) return label }() @@ -77,6 +78,7 @@ final class ListCell: UICollectionViewCell { label.textAlignment = .right label.textColor = .lightGray label.sizeToFit() + label.setContentCompressionResistancePriority(UILayoutPriority(800), for: .horizontal) return label }() // MARK: - Cell Initailize @@ -109,14 +111,13 @@ final class ListCell: UICollectionViewCell { NSLayoutConstraint.activate([ productImageView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor), productImageView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor), - productImageView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 10), - productImageView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor,constant: -10), - productImageView.heightAnchor.constraint(equalTo: productImageView.widthAnchor) + productImageView.widthAnchor.constraint(equalTo: self.contentView.widthAnchor, multiplier: 0.2), + productImageView.heightAnchor.constraint(equalTo: self.heightAnchor) ]) NSLayoutConstraint.activate([ verticalStackView.leadingAnchor.constraint(equalTo: productImageView.trailingAnchor, constant: 10), - verticalStackView.topAnchor.constraint(equalTo: productImageView.topAnchor), - verticalStackView.bottomAnchor.constraint(equalTo: productImageView.bottomAnchor), + verticalStackView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 5), + verticalStackView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -5), verticalStackView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -10) ]) } @@ -137,7 +138,7 @@ final class ListCell: UICollectionViewCell { self.productImageView.setContentCompressionResistancePriority(UILayoutPriority(1000), for: .vertical) } - private func setupPriceLabel(currency: Currency, price: Int, bargainPrice: Int) { + private func setupPriceLabel(currency: Currency, price: Double, bargainPrice: Double) { let upperCurreny = currency.rawValue.uppercased() if price == bargainPrice { let price = price.adoptDecimalStyle() diff --git a/OpenMarket/OpenMarket/View/PickerImageView.swift b/OpenMarket/OpenMarket/View/PickerImageView.swift new file mode 100644 index 000000000..7d8682de6 --- /dev/null +++ b/OpenMarket/OpenMarket/View/PickerImageView.swift @@ -0,0 +1,25 @@ +// +// PickerImageVIew.swift +// OpenMarket +// +// Created by 웡빙, 보리사랑 on 2022/07/28. +// + +import UIKit + +final class PickerImageView: UIImageView { + + override init(frame: CGRect) { + super.init(frame: frame) + setupImageView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupImageView() { + self.translatesAutoresizingMaskIntoConstraints = false + self.setFrame(at: 100) + } +} diff --git a/OpenMarket/OpenMarket/View/ProductSetupView.swift b/OpenMarket/OpenMarket/View/ProductSetupView.swift new file mode 100644 index 000000000..db6102197 --- /dev/null +++ b/OpenMarket/OpenMarket/View/ProductSetupView.swift @@ -0,0 +1,214 @@ +// +// ProductSetupView.swift +// OpenMarket +// +// Created by 웡빙, 보리사랑 on 2022/07/28. +// + +import UIKit + +final class ProductSetupView: UIView { + let mainScrollView: UIScrollView = { + var scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + return scrollView + }() + + lazy var mainStackView: UIStackView = { + var stackview = UIStackView(arrangedSubviews: [horizontalScrollView, + productNameTextField, + priceStackView, + productDiscountedPriceTextField, + productStockTextField, + descriptionTextView]) + stackview.translatesAutoresizingMaskIntoConstraints = false + stackview.axis = .vertical + stackview.distribution = .fill + stackview.alignment = .fill + stackview.spacing = 10 + return stackview + }() + + let horizontalScrollView: UIScrollView = { + var scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.showsHorizontalScrollIndicator = false + return scrollView + }() + + lazy var horizontalStackView: UIStackView = { + var stackview = UIStackView() + stackview.translatesAutoresizingMaskIntoConstraints = false + stackview.axis = .horizontal + stackview.distribution = .fill + stackview.alignment = .fill + stackview.spacing = 10 + return stackview + }() + + let addImageButton: UIButton = { + var button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(UIImage(systemName: "plus"), for: .normal) + button.backgroundColor = .lightGray + button.widthAnchor.constraint(equalToConstant: 100).isActive = true + button.heightAnchor.constraint(equalToConstant: 100).isActive = true + return button + }() + + lazy var productNameTextField: UITextField = { + var textfield = UITextField() + textfield.translatesAutoresizingMaskIntoConstraints = false + textfield.placeholder = "상품명" + textfield.setupLayer() + textfield.addLeftPadding() + textfield.inputAccessoryView = accessoryView + return textfield + }() + + lazy var priceStackView: UIStackView = { + var stackview = UIStackView(arrangedSubviews: [productPriceTextField, + currencySegmentControl]) + stackview.translatesAutoresizingMaskIntoConstraints = false + stackview.axis = .horizontal + stackview.distribution = .fill + stackview.alignment = .fill + stackview.spacing = 10 + return stackview + }() + + lazy var productPriceTextField: UITextField = { + var textfield = UITextField() + textfield.translatesAutoresizingMaskIntoConstraints = false + textfield.placeholder = "상품가격" + textfield.setupLayer() + textfield.addLeftPadding() + textfield.inputAccessoryView = accessoryView + return textfield + }() + + let currencySegmentControl: UISegmentedControl = { + var segment = UISegmentedControl(items: ["KRW","USD"]) + segment.translatesAutoresizingMaskIntoConstraints = false + segment.selectedSegmentIndex = 0 + return segment + }() + + lazy var productDiscountedPriceTextField: UITextField = { + var textfield = UITextField() + textfield.translatesAutoresizingMaskIntoConstraints = false + textfield.placeholder = "할인금액" + textfield.setupLayer() + textfield.addLeftPadding() + textfield.inputAccessoryView = accessoryView + return textfield + }() + + lazy var productStockTextField: UITextField = { + var textfield = UITextField() + textfield.translatesAutoresizingMaskIntoConstraints = false + textfield.placeholder = "재고수량" + textfield.setupLayer() + textfield.addLeftPadding() + textfield.inputAccessoryView = accessoryView + return textfield + }() + + lazy var descriptionTextView: UITextView = { + var textview = UITextView() + textview.translatesAutoresizingMaskIntoConstraints = false + textview.isScrollEnabled = false + textview.text = "여기에 내용을 입력해주세요." + textview.inputAccessoryView = accessoryView + return textview + }() + + let accessoryView: UIView = { + return UIView(frame: CGRect(x: 0.0, y: 0.0, width: UIScreen.main.bounds.width, height: 50)) + }() + + let confirmButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle("닫기", for: .normal) + button.setTitleColor(UIColor.black, for: .normal) + button.backgroundColor = .systemGray6 + return button + }() + + init(_ rootViewController: UIViewController) { + super.init(frame: .null) + addSubViews(rootViewController) + setupConstraints(rootViewController) + keyboardTypeSetup() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func addSubViews(_ rootViewController: UIViewController) { + rootViewController.view.addSubview(mainScrollView) + mainScrollView.addSubview(mainStackView) + horizontalScrollView.addSubview(horizontalStackView) + accessoryView.addSubview(confirmButton) + } + + private func setupConstraints(_ rootViewController: UIViewController) { + let mainStackViewHeightAnchor = mainStackView.heightAnchor.constraint(equalTo: mainScrollView.frameLayoutGuide.heightAnchor) + mainStackViewHeightAnchor.priority = UILayoutPriority(rawValue: UILayoutPriority.defaultLow.rawValue) + + let horizontalStackViewWidthAnchor = horizontalStackView.widthAnchor.constraint(equalTo: horizontalScrollView.frameLayoutGuide.widthAnchor) + horizontalStackViewWidthAnchor.priority = UILayoutPriority(rawValue: UILayoutPriority.defaultLow.rawValue) + + NSLayoutConstraint.activate([ + mainScrollView.topAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.topAnchor), + mainScrollView.bottomAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.bottomAnchor), + mainScrollView.leadingAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.leadingAnchor), + mainScrollView.trailingAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.trailingAnchor) + ]) + NSLayoutConstraint.activate([ + mainStackViewHeightAnchor, + mainStackView.topAnchor.constraint(equalTo: mainScrollView.contentLayoutGuide.topAnchor), + mainStackView.bottomAnchor.constraint(equalTo: mainScrollView.contentLayoutGuide.bottomAnchor), + mainStackView.leadingAnchor.constraint(equalTo: mainScrollView.contentLayoutGuide.leadingAnchor, constant: 10), + mainStackView.trailingAnchor.constraint(equalTo: mainScrollView.contentLayoutGuide.trailingAnchor, constant: -10), + mainStackView.widthAnchor.constraint(equalTo: mainScrollView.widthAnchor, constant: -20) + ]) + NSLayoutConstraint.activate([ + horizontalScrollView.heightAnchor.constraint(equalToConstant: 100), + horizontalScrollView.topAnchor.constraint(equalTo: mainStackView.topAnchor), + horizontalScrollView.bottomAnchor.constraint(equalTo: productNameTextField.topAnchor, constant: -10), + horizontalScrollView.leadingAnchor.constraint(equalTo: mainStackView.leadingAnchor), + horizontalScrollView.trailingAnchor.constraint(equalTo: mainStackView.trailingAnchor) + ]) + NSLayoutConstraint.activate([ + horizontalStackViewWidthAnchor, + horizontalStackView.topAnchor.constraint(equalTo: horizontalScrollView.contentLayoutGuide.topAnchor), + horizontalStackView.bottomAnchor.constraint(equalTo: horizontalScrollView.contentLayoutGuide.bottomAnchor), + horizontalStackView.heightAnchor.constraint(equalTo: horizontalScrollView.heightAnchor), + horizontalStackView.leadingAnchor.constraint(equalTo: horizontalScrollView.contentLayoutGuide.leadingAnchor), + horizontalStackView.trailingAnchor.constraint(equalTo: horizontalScrollView.contentLayoutGuide.trailingAnchor) + ]) + NSLayoutConstraint.activate([ + productNameTextField.heightAnchor.constraint(equalToConstant: 35), + productPriceTextField.heightAnchor.constraint(equalToConstant: 35), + productPriceTextField.widthAnchor.constraint(equalTo: priceStackView.widthAnchor, multiplier: 0.7), + productDiscountedPriceTextField.heightAnchor.constraint(equalToConstant: 35), + productStockTextField.heightAnchor.constraint(equalToConstant: 35) + ]) + guard let confirmButtonSuperview = confirmButton.superview else { return } + NSLayoutConstraint.activate([ + confirmButton.leadingAnchor.constraint(equalTo: confirmButtonSuperview.leadingAnchor, constant: 350), + confirmButton.trailingAnchor.constraint(equalTo: confirmButtonSuperview.trailingAnchor), + confirmButton.bottomAnchor.constraint(equalTo: confirmButtonSuperview.bottomAnchor), + confirmButton.heightAnchor.constraint(equalToConstant: 30) + ]) + } + + private func keyboardTypeSetup() { + productPriceTextField.keyboardType = .numberPad + productDiscountedPriceTextField.keyboardType = .numberPad + productStockTextField.keyboardType = .numberPad + } +} diff --git a/OpenMarketClassDiagram.drawio.png b/OpenMarketClassDiagram.drawio.png deleted file mode 100644 index 0e97423a5..000000000 Binary files a/OpenMarketClassDiagram.drawio.png and /dev/null differ diff --git a/OpenMarketClassDiagram.png b/OpenMarketClassDiagram.png new file mode 100644 index 000000000..065d381ab Binary files /dev/null and b/OpenMarketClassDiagram.png differ diff --git a/README.md b/README.md index 35268d039..be8c614e7 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ ### 타임라인 #### Week 1 +
+ - **2022-07-11(월)** - STEP1-1: 데이터를 받아줄 모델타입 구현 - STEP1-1: decode과정 unit 테스트 @@ -39,8 +41,12 @@ - **2022-07-15(금)** - Modern Collection View 기반으로 서버에서 fetch한 데이터를 출력하도록 변경 - 하나의 CollectionView에서 Layout만을 변경하여 사용할 수 있게 구현 - + +
+ #### Week 2 +
+ - **2022-07-18(월)** - STEP2-2: 가격레이블의 조건별 UI 지정, 네비게이션 바에 +버튼 추가, List 뷰에서 스크롤 시 셀높이 축소현상에 관련한 오토레이아웃 재설정 시도 - **2022-07-19(화)** - STEP2 PR @@ -52,34 +58,65 @@ - **2022-07-21(목)** - STEP2 피드백 수정: 불필요한 주석제거, 접근제어자 변경, 중복 메서드 제거, activity indicator 계층구조를 변경 - **2022-07-22(금)** - - BonusSTEP: 로컬캐싱 구현 시도중 + - BonusSTEP: 로컬캐싱 구현 +
+ +#### Week 3 +
+ +- **2022-07-25(월)** + - STEP 3: HTTP Method(Get) 구현 +- **2022-07-26(화)** - STEP2 PR + - STEP 3: HTTP Method(Post) 구현 +- **2022-07-27(수)** + - STEP 3: HTTP Method(PATCH, DELETE) 구현 +- **2022-07-28(목)** + - 상품을 등록,수정하는 뷰구현 + - 기기의 앨범에 접근하여 사진을 가져오는 UIPickerView 이용 +- **2022-07-29(금)** + - 셀 선택시 수정 화면으로 넘어가도록 구현 + - 텍스트 필드 별 맞춤 키보드 및 키보드가 Content를 가리지 않도록 구현 중 +
+ ### UML **Class Diagram(220713 STEP1 작성 기준)** -![](https://i.imgur.com/zx8UY5K.png) + + ### FileTree ``` + ├── OpenMarket +│ ├── Model +│ │ ├── ProductPage +│ │ ├── Product +│ │ ├── Currency +│ │ ├── ProductDetail +│ │ ├── ProductRegistration +│ │ └── ModificationData +│ ├── Controller +│ │ ├── MainViewController +│ │ └── ProductSetupViewController +│ ├── View +│ │ ├── ProductSetupView +│ │ ├── ListCell +│ │ ├── GridCell +│ │ └── PickerImageView │ ├── Extensions -│ │ ├── Int+Extensions +│ │ ├── Double+Extensions │ │ ├── UIImageView+Extensions -│ │ └── UILabel+Extensions +│ │ ├── UILabel+Extensions +│ │ └── UITextField+Extensions │ ├── Command │ │ └── URLCommand │ ├── Utility │ │ ├── JsonDecoder -│ │ └── URLSession -│ ├── Model -│ │ ├── ProductPage -│ │ ├── Product -│ │ └── Currency +│ │ ├── URLSession +│ │ └── ImageCacheManager │ ├── AppDelegate -│ ├── SceneDelegate -│ ├── MainViewController -│ ├── ListCell -│ └── GridCell +│ └── SceneDelegate └── DataFetchTests └── DataFetchTests ``` @@ -87,10 +124,16 @@ [STEP1](https://github.com/yagom-academy/ios-open-market/pull/174) [STEP2](https://github.com/yagom-academy/ios-open-market/pull/183) [STEP3]() + ### UI #### Cell -![](https://i.imgur.com/PxAAPtX.jpg) -![](https://i.imgur.com/EHA2lc0.jpg) + + + +#### 상품등록,수정 View + + + ### 기능 설명 - 상품 상세정보를 표현할 `Product`, 상품 리스트를 받아올 `ProductPage` 데이터 타입을 구현. @@ -101,19 +144,41 @@ - `SegmentController`를 `NavigationItem`의 title view로 지정하여 각 화면을 이동하도록 함 - ModernCollection View를 활용하여 리스트 형태와 그리드 형태로 데이터를 출력하도록 구현 - `ActivityIndicator`를 활용하여 데이터 로드시에 작업중임을 나타내게 구현 +- URLSession 프레임워크를 활용하여 HTTP METHOD(GET, POST, PATCH, DELETE)를 구현, 서버로부터 데이터 통신하도록 함 + - 통신시 디코딩될 데이터 타입을 구현(ProductDetail, ProductPage, etc) +- UIImagePickerView를 통하여 기기의 사진첩에 접근, 서버에 등록할 제품의 이미지를 선택하도록 구현 +- TextField별로 다른 종류의 키보드로 타이핑 할 수 있도록 함. + ### 트러블 슈팅 -- URLSession dataTask 시점 맞추기 -> 해결 +- URLSession dataTask 시점 맞추기 -> **해결** - dataTask.resume() 실행 후 바로 데이터가 바로 받아와 지는 것이 아님을 인지. - 네트워킹한 데이터에 접근하는 메서드에서 nil 오류 -> 이는 비동기적으로 작동하면서 출력되는 현상임을 확인 - 네트워킹시간과 UI업데이트 로직을 현재 정확하게 이해하여 원하는 시점에서 자유롭게 사용할 수 있도록 코드를 다시 분석함. -- modern Collection View 레이아웃 교체 이슈 -> 해결 +- modern Collection View 레이아웃 교체 이슈 -> **해결** - 세그먼트 전환시, 현재있던 하위뷰를 제거해준 뒤 세그먼트에 해당하는 layout과 dataSource 를 모두 재생성 해주며 뷰 전환 - 뷰 전환마다 네트워킹이 이루어져 비용적 측면에서 좋지 않다고 판단 - 최초에 데이터 로드시 데이터를 fetch하며, 이후 새로고침, 목록 추가와 같은 네트워킹이 필요한 시점에만 fetch하는 메서드를 호출하면 될것으로 예상 -- modern Collection View 이미지 축소 이슈 -> 해결 +- modern Collection View 이미지 축소 이슈 -> **해결** - list view 로드 시, 몇몇 셀의 축소현상 발생 - 스크롤을 맨밑까지 한번 갔다오면 축소되었던 셀 정상화 - 최초에는 기본적으로 apple에서 제공해주는 리스트 레이아웃 코드를 사용하여 코드를 작성했음, 다만 default List layout에서 custom Cell 을 사용하는것 + reuse 되면서 일어나는 문제라고 판단함 - List Layout 자체를 compositionalLayout을 활용하여 작성함으로써 해결. +- 가격을 받아오는 타입이 Int이지만 , 서버에 올라간 데이터가 Double 타입일 때 HTTP GET 오류 -> **해결** + - 캠퍼 중 한명이 처음으로 Double 타입의 숫자(0.75$) 로 게시물을 올렸는데, 이 하나 때문에 전체 데이터가 받아와지지 않았다. + - 가격을 받아오는 타입을 Double로 변경해주어 해결 + +- IPhone 12 미니 스크롤뷰 이슈 -> **확인 중** + - 다른 시뮬레이터 기기 및 실기기(Iphone 8+)에서는 확인 되지 않으나 아이폰 12 미니를 시뮬레이터로 동작시킬 경우 UIImagePickerController로 부터 받아온 이미지를 담는 HorizonalStackview에 알수 없는 선이 생성됨 + - + - 다른 시뮬레이터에서는 확인 되지 않아 단순 시뮬레이터 오류인지 확인 중 + +- text/Plain 형태의 서버 response 디코딩 이슈 -> **해결** + - 기존의 데이터 타입은 application/json형태로 response의 data가 옴. + - 다만 DELETE를 위해 SECRET 키를 발급 받는 과정에서는 text/plain 형태로 와서 기존의 JSON 형태만을 decoding하는 decode 메서드로는 처리가 불가 + - text/plain 형태의 응답 데이터도 String 타입으로 바로 디코딩해줄 수 있도록 변경함. + +- 키보드가 컨텐츠 영역을 가리지 않도록 프레임의 y값을 수정 -> **해결 중** + - 키보드가 올라오는 시점에서 키보드 view의 height값을 불러와 그만큼 view의 y값이 올라가게 지정 하려함 + - 키보드가 화면에 나오게 했을 때 올라가게 구현하였으나, 제대로 올라가지 않는 이슈 해결 중