Skip to content

Commit

Permalink
feat: ✨New Issue 저장 전까지 구현(Lia316#106, Lia316#134)
Browse files Browse the repository at this point in the history
이슈 목록창에서 '+' 버튼 클릭시 새로운 이슈를 작성할 수 있는 View Controller로 이동하며 마크다운 형식으로 작성할시 Preview를 통해 확인 할 수 있다.

텍스트 뷰를 길게 탭할시 'Insert Photo'를 지원하며 성공적으로 업로드 되면 바로 마크다운 형식으로 텍스트뷰에 삽입된다
  • Loading branch information
ghis22130 committed Jun 22, 2021
1 parent 9c60181 commit f312e33
Show file tree
Hide file tree
Showing 17 changed files with 663 additions and 18 deletions.
32 changes: 32 additions & 0 deletions ios/IssueTracker/IssueTracker.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
FF79E029267C75130088D2C7 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF79E028267C75130088D2C7 /* PreviewViewController.swift */; };
FF79E02B267C756D0088D2C7 /* NewIssueViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF79E02A267C756D0088D2C7 /* NewIssueViewModel.swift */; };
FF79E02D267C75B60088D2C7 /* PostNewIssueUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF79E02C267C75B60088D2C7 /* PostNewIssueUseCase.swift */; };
FF79E02F267C7D280088D2C7 /* Filterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF79E02E267C7D280088D2C7 /* Filterable.swift */; };
FF79E03126801F250088D2C7 /* FetchFilterSectionsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF79E03026801F250088D2C7 /* FetchFilterSectionsUseCase.swift */; };
FF79E03426801F450088D2C7 /* FilterItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF79E03326801F450088D2C7 /* FilterItem.swift */; };
FF79E036268029970088D2C7 /* FiletringHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF79E035268029970088D2C7 /* FiletringHeader.swift */; };
FF79E038268069060088D2C7 /* UploadImageUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF79E037268069060088D2C7 /* UploadImageUseCase.swift */; };
FF79E03A26806CCE0088D2C7 /* ImageFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF79E03926806CCE0088D2C7 /* ImageFile.swift */; };
FF89A95E266F0BD000983577 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF89A95D266F0BD000983577 /* AppDelegate.swift */; };
FF89A960266F0BD000983577 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF89A95F266F0BD000983577 /* SceneDelegate.swift */; };
FF89A962266F0BD000983577 /* IssueListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF89A961266F0BD000983577 /* IssueListViewController.swift */; };
Expand Down Expand Up @@ -111,6 +117,12 @@
FF79E028267C75130088D2C7 /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = "<group>"; };
FF79E02A267C756D0088D2C7 /* NewIssueViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewIssueViewModel.swift; sourceTree = "<group>"; };
FF79E02C267C75B60088D2C7 /* PostNewIssueUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostNewIssueUseCase.swift; sourceTree = "<group>"; };
FF79E02E267C7D280088D2C7 /* Filterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filterable.swift; sourceTree = "<group>"; };
FF79E03026801F250088D2C7 /* FetchFilterSectionsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchFilterSectionsUseCase.swift; sourceTree = "<group>"; };
FF79E03326801F450088D2C7 /* FilterItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterItem.swift; sourceTree = "<group>"; };
FF79E035268029970088D2C7 /* FiletringHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiletringHeader.swift; sourceTree = "<group>"; };
FF79E037268069060088D2C7 /* UploadImageUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadImageUseCase.swift; sourceTree = "<group>"; };
FF79E03926806CCE0088D2C7 /* ImageFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFile.swift; sourceTree = "<group>"; };
FF89A95A266F0BD000983577 /* IssueTracker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IssueTracker.app; sourceTree = BUILT_PRODUCTS_DIR; };
FF89A95D266F0BD000983577 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
FF89A95F266F0BD000983577 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -208,6 +220,7 @@
AE7505322670EE0600BA14F7 /* UIColor+hexToUIColor.swift */,
FF76F8B42677300300342633 /* UIBarButtonItem+setTitle.swift */,
FF76F8CD2678739B00342633 /* ViewControllerIdentifierable.swift */,
FF79E02E267C7D280088D2C7 /* Filterable.swift */,
);
path = Utility;
sourceTree = "<group>";
Expand Down Expand Up @@ -277,6 +290,7 @@
FF79E01E267C74AA0088D2C7 /* NewIssue */ = {
isa = PBXGroup;
children = (
FF79E03226801F3B0088D2C7 /* Model */,
FF79E022267C74C30088D2C7 /* View */,
FF79E021267C74BD0088D2C7 /* ViewController */,
FF79E020267C74B50088D2C7 /* ViewModel */,
Expand All @@ -289,6 +303,8 @@
isa = PBXGroup;
children = (
FF79E02C267C75B60088D2C7 /* PostNewIssueUseCase.swift */,
FF79E03026801F250088D2C7 /* FetchFilterSectionsUseCase.swift */,
FF79E037268069060088D2C7 /* UploadImageUseCase.swift */,
);
path = UseCase;
sourceTree = "<group>";
Expand All @@ -313,6 +329,7 @@
FF79E022267C74C30088D2C7 /* View */ = {
isa = PBXGroup;
children = (
FF79E035268029970088D2C7 /* FiletringHeader.swift */,
);
path = View;
sourceTree = "<group>";
Expand All @@ -326,6 +343,15 @@
path = Container;
sourceTree = "<group>";
};
FF79E03226801F3B0088D2C7 /* Model */ = {
isa = PBXGroup;
children = (
FF79E03326801F450088D2C7 /* FilterItem.swift */,
FF79E03926806CCE0088D2C7 /* ImageFile.swift */,
);
path = Model;
sourceTree = "<group>";
};
FF89A951266F0BD000983577 = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -638,13 +664,17 @@
AED83A4B2677453A00C36CA4 /* NetworkComponents.swift in Sources */,
FF79E024267C74F00088D2C7 /* NewIssueViewController.swift in Sources */,
AECEA445266F3AD900855F20 /* IssueCell.swift in Sources */,
FF79E03426801F450088D2C7 /* FilterItem.swift in Sources */,
FF79E02F267C7D280088D2C7 /* Filterable.swift in Sources */,
AE7D39432672033800BF9487 /* UIView+HiddenWithAnimation.swift in Sources */,
FF76F8CC2678704200342633 /* SceneFlowCoordinator.swift in Sources */,
AECEA442266F38D000855F20 /* IssueDataSource.swift in Sources */,
FF89A962266F0BD000983577 /* IssueListViewController.swift in Sources */,
FF89A95E266F0BD000983577 /* AppDelegate.swift in Sources */,
AED83A4926772D6600C36CA4 /* NetworkService.swift in Sources */,
FF76F8D12678772000342633 /* FetchIssueListUseCase.swift in Sources */,
FF79E038268069060088D2C7 /* UploadImageUseCase.swift in Sources */,
FF79E03A26806CCE0088D2C7 /* ImageFile.swift in Sources */,
FF79E02D267C75B60088D2C7 /* PostNewIssueUseCase.swift in Sources */,
AEE2A4D32670A00300FA6274 /* IssueViewModel.swift in Sources */,
FF79E029267C75130088D2C7 /* PreviewViewController.swift in Sources */,
Expand All @@ -653,6 +683,7 @@
AE7505312670ED1F00BA14F7 /* UILabel+AddLineSpacing.swift in Sources */,
FF79E027267C750A0088D2C7 /* MarkdownViewController.swift in Sources */,
FF89A99F2670793B00983577 /* IssueListMockData.swift in Sources */,
FF79E03126801F250088D2C7 /* FetchFilterSectionsUseCase.swift in Sources */,
FF76F8CE2678739B00342633 /* ViewControllerIdentifierable.swift in Sources */,
FF76F8C82678701000342633 /* IssueTrackerDIContainer.swift in Sources */,
FF89A960266F0BD000983577 /* SceneDelegate.swift in Sources */,
Expand All @@ -661,6 +692,7 @@
FF89A999267077E800983577 /* User.swift in Sources */,
AEE2A4D52670A8C000FA6274 /* IssueLabel.swift in Sources */,
AE7505332670EE0600BA14F7 /* UIColor+hexToUIColor.swift in Sources */,
FF79E036268029970088D2C7 /* FiletringHeader.swift in Sources */,
FF89A997267075CD00983577 /* Issue.swift in Sources */,
FF89A99D2670784200983577 /* Milestone.swift in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import UIKit

final class IssueTrackerDIContainer: SceneFlowCoordinatorDependencies {
private let networkManager = NetworkManager()

private func makeFetchIssueListUseCase() -> FetchIssueListUseCase {
return DefaultFetchIssueListUseCase(networkManager: networkManager)
}
Expand All @@ -36,3 +36,36 @@ final class IssueTrackerDIContainer: SceneFlowCoordinatorDependencies {
return SceneFlowCoordinator(rootViewController, self)
}
}

//MARK: - NewIssue ViewController

extension IssueTrackerDIContainer {
private func makePostNewIssueUseCase() -> PostNewIssueUseCase {
return DefaultPostNewIssueUseCase(networkManager)
}

private func makeFetchFilterSectionsUseCase() -> FetchFilterSectionsUseCase {
return DefaultFetchFilterSectionsUseCase(networkManager)
}

private func makePostImageFileUseCase() -> UploadImageUseCase {
return DefaultUploadImageUseCase(networkManager)
}

private func makeNewIssueViewModel() -> NewIssueViewModel {
return NewIssueViewModel(makeFetchFilterSectionsUseCase() ,makePostNewIssueUseCase(), makePostImageFileUseCase())
}

private func makeMarkdownViewController(_ viewModel: NewIssueViewModel) -> MarkdownViewController {
return MarkdownViewController.create(viewModel)
}

private func makePreviewViewController() -> PreviewViewController {
return PreviewViewController.create()
}

func makeNewIssueViewController() -> NewIssueViewController {
let viewModel = makeNewIssueViewModel()
return NewIssueViewController.create(viewModel, makeMarkdownViewController(viewModel), makePreviewViewController())
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
//
// SceneFlowCoordinator.swift
// IssueTracker
//
// Created by 지북 on 2021/06/15.
//

import UIKit

protocol SceneFlowCoordinatorDependencies {
func makeIssueListTabBarController(_ viewControllers: [UIViewController]) -> UITabBarController
func makeIssueListNavigationController(_ action: IssueListViewControllerAction) -> UINavigationController
func makeNewIssueViewController() -> NewIssueViewController
}


final class SceneFlowCoordinator {
class SceneFlowCoordinator {
private weak var rootVC: UINavigationController?
private var dependencies: SceneFlowCoordinatorDependencies
private var issueListViewController: UINavigationController?
Expand All @@ -33,5 +27,8 @@ final class SceneFlowCoordinator {
}

func showNewIssueView() {
guard let issueListViewController = issueListViewController else { return }
let vc = dependencies.makeNewIssueViewController()
issueListViewController.pushViewController(vc, animated: true)
}
}
12 changes: 9 additions & 3 deletions ios/IssueTracker/IssueTracker/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,9 @@
</constraints>
</view>
<connections>
<outlet property="containerView" destination="MHz-XW-UnV" id="4VJ-ia-Nrl"/>
<outlet property="filteringTableView" destination="2hJ-fB-XAx" id="5II-d2-tLh"/>
<outlet property="titleTextFiled" destination="LkM-k2-snb" id="4KC-aI-5z2"/>
<outlet property="containerView" destination="MHz-XW-UnV" id="310-lq-p4c"/>
<outlet property="filteringTableView" destination="2hJ-fB-XAx" id="oDe-0K-Y6c"/>
<outlet property="titleTextField" destination="tL3-Pe-1ke" id="lvi-zV-CBh"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="3rO-8X-2fK" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
Expand Down Expand Up @@ -234,6 +234,9 @@
<constraint firstItem="eFi-se-PvF" firstAttribute="leading" secondItem="jpy-7i-GUv" secondAttribute="leading" id="dpg-Mi-dJI"/>
</constraints>
</view>
<connections>
<outlet property="textView" destination="eFi-se-PvF" id="GrB-sK-GDI"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Bo8-ea-7KT" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
Expand Down Expand Up @@ -261,6 +264,9 @@
<constraint firstItem="IEV-rY-Cqn" firstAttribute="bottom" secondItem="Htv-VU-X7W" secondAttribute="bottom" id="mV9-nL-xS3"/>
</constraints>
</view>
<connections>
<outlet property="markdownView" destination="Htv-VU-X7W" id="7cT-D8-oDf"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="pLm-9c-UVK" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//
// Filterable.swift
// IssueTracker
//
// Created by 지북 on 2021/06/18.
//

import Foundation
56 changes: 56 additions & 0 deletions ios/IssueTracker/IssueTracker/Network/NetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Combine
protocol NetworkManageable {
func get<T: Decodable>(path: String, type: T.Type) -> AnyPublisher<T, NetworkError>
func post<T: Encodable, R: Decodable>(path: String, data: T, result: R.Type) -> AnyPublisher<R, NetworkError>
func imageUpload<R: Decodable>(path: String, data: Data?, result: R.Type) -> AnyPublisher<R, NetworkError>
}


Expand Down Expand Up @@ -42,6 +43,22 @@ extension NetworkManager: NetworkManageable {
return request(url: url, data: data, result: result)
}

func imageUpload<R: Decodable>(path: String, data: Data?, result: R.Type) -> AnyPublisher<R, NetworkError> {
guard let url = EndPoint.url(path: path) else {
return Fail(error: NetworkError.BadURL).eraseToAnyPublisher()
}
print(url.absoluteURL)
guard let data = data else { return Fail(error: NetworkError.BadRequest).eraseToAnyPublisher() }
let boundary = generateBoundaryString()
let bodyData = buildBody(boundary: boundary,
fieldName: "file",
data: data,
mimType: "image/jpeg",
fileName: "\(Date().description).jpg")

return request(url: url, data: bodyData, result: result, boundary: boundary)
}

}


Expand Down Expand Up @@ -91,3 +108,42 @@ extension NetworkManager {
}

}

extension NetworkManager {

func request<R: Decodable>(url: URL, data: Data, result: R.Type, boundary: String) -> AnyPublisher<R, NetworkError> {

return Just(data)
.map { data -> URLRequest in
var request = URLRequest(url: url)
request.httpMethod = HTTPMethodType.post
request.httpBody = data
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
return request
}
.flatMap { request in
return self.request(with: request, type: R.self)
}.eraseToAnyPublisher()
}
}

extension NetworkManager {

private func generateBoundaryString() -> String {
return "Boundary-\(UUID().uuidString)"
}

private func buildBody(boundary: String, fieldName: String, data: Data, mimType: String, fileName: String) -> Data {
let headerLines = ["--\(boundary)",
"Content-Disposition: form-data; name=\"\(fieldName)\"; filename=\"\(fileName)\"",
"Content-Type: \(mimType)",
"\r\n"
]
let testString = headerLines.joined(separator: "\r\n") + "\r\n--\(boundary)--"
var bodyData = headerLines.joined(separator: "\r\n").data(using: .utf8)!
bodyData.append(data)
bodyData.append(contentsOf: "\r\n--\(boundary)--\r\n".data(using:.utf8)!)

return bodyData
}
}
13 changes: 13 additions & 0 deletions ios/IssueTracker/IssueTracker/NewIssue/Model/FilterItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// FilterItem.swift
// IssueTracker
//
// Created by 지북 on 2021/06/21.
//

import Foundation

struct FilterItem: Decodable {
let id: Int
let name: String
}
18 changes: 18 additions & 0 deletions ios/IssueTracker/IssueTracker/NewIssue/Model/ImageFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// ImageFile.swift
// IssueTracker
//
// Created by 지북 on 2021/06/21.
//

import Foundation

struct ImageFile: Decodable {
let id: Int
let name: String
let path: String

func markdownImagePath() -> String {
return "![\(name)](https://issue-tracker-swagger.herokuapp.com\(path))"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// FetchFilterSectionsUseCase.swift
// IssueTracker
//
// Created by 지북 on 2021/06/21.
//

import UIKit
import Combine

protocol FetchFilterSectionsUseCase {
func excute(completion: @escaping (Result<FilteringSection, NetworkError>) -> Void)
}


final class DefaultFetchFilterSectionsUseCase: FetchFilterSectionsUseCase {

private var networkManager: NetworkManageable
private var cancelBag = Set<AnyCancellable>()

init(_ networkManager: NetworkManageable) {
self.networkManager = networkManager
}

func excute(completion: @escaping (Result<FilteringSection, NetworkError>) -> Void) {

networkManager.get(path: "/labels", type: [FilterItem].self)
.receive(on: DispatchQueue.main)
.sink { error in
switch error {
case .failure(let error): completion(.failure(error))
case .finished: break
}
} receiveValue: { items in
let section = FilteringSection.init(name: "Label", items: items)
completion(.success(section))
}.store(in: &cancelBag)

networkManager.get(path: "/milestones", type: [FilterItem].self)
.receive(on: DispatchQueue.main)
.sink { error in
switch error {
case .failure(let error): completion(.failure(error))
case .finished: break
}
} receiveValue: { items in
let section = FilteringSection.init(name: "Milestone", items: items)
completion(.success(section))
}.store(in: &cancelBag)

networkManager.get(path: "/users", type: [FilterItem].self)
.receive(on: DispatchQueue.main)
.sink { error in
switch error {
case .failure(let error): completion(.failure(error))
case .finished: break
}
} receiveValue: { items in
let section = FilteringSection.init(name: "Assignee", items: items)
completion(.success(section))
}.store(in: &cancelBag)
}

}
Loading

0 comments on commit f312e33

Please sign in to comment.