From fab85f1cc8c7d3b451d747361ea9e98add2beeb4 Mon Sep 17 00:00:00 2001 From: taipaise Date: Sat, 22 Nov 2025 13:53:42 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20NetworkService=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - request 생성로직 수정. body에 jsonData 뿐 아니라, rawdata도 들어갈 수 있도록 수정 (s3 업로드 위함) - response body가 empty여도 error를 throw 하지 않도록 수정 --- .../DataSource/Sources/Common/Enum/Endpoint.swift | 9 +++++++++ .../Sources/Common/Enum/EndpointBodyType.swift | 11 +++++++++++ .../Sources/NetworkService/Extension/Endpoint+.swift | 9 ++++++++- .../Sources/NetworkService/NetworkService.swift | 4 +++- 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 Projects/DataSource/Sources/Common/Enum/EndpointBodyType.swift diff --git a/Projects/DataSource/Sources/Common/Enum/Endpoint.swift b/Projects/DataSource/Sources/Common/Enum/Endpoint.swift index cd47d6b5..d07631f8 100644 --- a/Projects/DataSource/Sources/Common/Enum/Endpoint.swift +++ b/Projects/DataSource/Sources/Common/Enum/Endpoint.swift @@ -5,6 +5,8 @@ // Created by 최정인 on 6/21/25. // +import Foundation + public protocol Endpoint { var baseURL: String { get } var path: String { get } @@ -13,4 +15,11 @@ public protocol Endpoint { var queryParameters: [String: String] { get } var bodyParameters: [String: Any] { get } var isAuthorized: Bool { get } + var bodyType: EndpointBodyType { get } + var bodyData: Data? { get } +} + +extension Endpoint { + var bodyType: EndpointBodyType { return .json } + var bodyData: Data? { return nil } } diff --git a/Projects/DataSource/Sources/Common/Enum/EndpointBodyType.swift b/Projects/DataSource/Sources/Common/Enum/EndpointBodyType.swift new file mode 100644 index 00000000..6a2a521c --- /dev/null +++ b/Projects/DataSource/Sources/Common/Enum/EndpointBodyType.swift @@ -0,0 +1,11 @@ +// +// EndpointBodyType.swift +// DataSource +// +// Created by 이동현 on 11/22/25. +// + +public enum EndpointBodyType { + case json + case rawData +} diff --git a/Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift b/Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift index 83ab723f..87446ef3 100644 --- a/Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift +++ b/Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift @@ -12,7 +12,14 @@ extension Endpoint { var request = try URLRequest(urlString: path, queryParameters: queryParameters) request.httpMethod = method.rawValue request.makeHeaders(headers: headers) - try request.makeBodyParameter(with: bodyParameters) + switch bodyType { + case .json: + try request.makeBodyParameter(with: bodyParameters) + case .rawData: + if let data = bodyData { + request.httpBody = data + } + } request.cachePolicy = .reloadIgnoringLocalCacheData return request } diff --git a/Projects/DataSource/Sources/NetworkService/NetworkService.swift b/Projects/DataSource/Sources/NetworkService/NetworkService.swift index 8ae62808..aa98c779 100644 --- a/Projects/DataSource/Sources/NetworkService/NetworkService.swift +++ b/Projects/DataSource/Sources/NetworkService/NetworkService.swift @@ -78,7 +78,9 @@ final class NetworkService { throw NetworkError.invalidStatusCode(statusCode: httpResponse.statusCode) } - guard !data.isEmpty else { throw NetworkError.emptyData } + if T.self == EmptyResponseDTO.self { + return EmptyResponseDTO() as? T + } do { let bitnagilResponse = try decoder.decode(BaseResponse.self, from: data) From 898e3e5625400effc98798d816eead7c07eab9e1 Mon Sep 17 00:00:00 2001 From: taipaise Date: Sat, 22 Nov 2025 16:28:51 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=EC=A0=9C=EB=B3=B4=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataSourceDependencyAssembler.swift | 4 + .../DTO/FilePresignedConditaionDTO.swift | 11 +++ .../Sources/DTO/FilePresignedDTO.swift | 18 +++++ .../DataSource/Sources/DTO/ReportDTO.swift | 8 +- .../Endpoint/FilePresignedEndpoint.swift | 68 +++++++++++++++++ .../Sources/Endpoint/ReportEndpoint.swift | 19 +++-- .../Sources/Endpoint/S3UploadEndpoint.swift | 63 ++++++++++++++++ .../Sources/Repository/FileRepository.swift | 25 +++++++ .../Sources/Repository/ReportRepository.swift | 26 ++++++- .../Sources/DomainDependencyAssembler.swift | 8 +- .../Sources/Entity/Enum/ReportType.swift | 6 ++ .../Repository/FIileRepositoryProtocol.swift | 23 ++++++ .../Repository/ReportRepositoryProtocol.swift | 18 ++++- .../UseCase/ReportUseCaseProtocol.swift | 14 +++- .../UseCase/Report/ReportUseCase.swift | 55 +++++++++++++- .../Home/View/HomeViewController.swift | 12 +-- .../View/ReportLoadingViewController.swift | 10 +++ .../ReportRegistrationViewController.swift | 31 ++++++++ .../ReportRegistrationViewModel.swift | 74 ++++++++++++++++++- 19 files changed, 465 insertions(+), 28 deletions(-) create mode 100644 Projects/DataSource/Sources/DTO/FilePresignedConditaionDTO.swift create mode 100644 Projects/DataSource/Sources/DTO/FilePresignedDTO.swift create mode 100644 Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift create mode 100644 Projects/DataSource/Sources/Endpoint/S3UploadEndpoint.swift create mode 100644 Projects/DataSource/Sources/Repository/FileRepository.swift create mode 100644 Projects/Domain/Sources/Protocol/Repository/FIileRepositoryProtocol.swift diff --git a/Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift b/Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift index fb82e010..8e21d308 100644 --- a/Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift +++ b/Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift @@ -48,5 +48,9 @@ public struct DataSourceDependencyAssembler: DependencyAssemblerProtocol { DIContainer.shared.register(type: ReportRepositoryProtocol.self) { _ in return ReportRepository() } + + DIContainer.shared.register(type: FileRepositoryProtocol.self) { _ in + return FileRepository() + } } } diff --git a/Projects/DataSource/Sources/DTO/FilePresignedConditaionDTO.swift b/Projects/DataSource/Sources/DTO/FilePresignedConditaionDTO.swift new file mode 100644 index 00000000..451561fb --- /dev/null +++ b/Projects/DataSource/Sources/DTO/FilePresignedConditaionDTO.swift @@ -0,0 +1,11 @@ +// +// FilePresignedDTO.swift +// DataSource +// +// Created by 이동현 on 11/22/25. +// + +struct FilePresignedConditaionDTO: Codable { + let prefix: String? + let fileName: String +} diff --git a/Projects/DataSource/Sources/DTO/FilePresignedDTO.swift b/Projects/DataSource/Sources/DTO/FilePresignedDTO.swift new file mode 100644 index 00000000..a992e63e --- /dev/null +++ b/Projects/DataSource/Sources/DTO/FilePresignedDTO.swift @@ -0,0 +1,18 @@ +// +// FilePresignedDTO.swift +// DataSource +// +// Created by 이동현 on 11/22/25. +// + +struct FilePresignedDTO: Decodable { + let file1: String? + let file2: String? + let file3: String? + + enum CodingKeys: String, CodingKey { + case file1 = "additionalProp1" + case file2 = "additionalProp2" + case file3 = "additionalProp3" + } +} diff --git a/Projects/DataSource/Sources/DTO/ReportDTO.swift b/Projects/DataSource/Sources/DTO/ReportDTO.swift index 882ccbd8..e74588b0 100644 --- a/Projects/DataSource/Sources/DTO/ReportDTO.swift +++ b/Projects/DataSource/Sources/DTO/ReportDTO.swift @@ -7,13 +7,13 @@ import Domain -struct ReportDTO: Decodable { +struct ReportDTO: Codable { let reportId: Int? let reportDate: String? let reportTitle: String let reportContent: String? let reportLocation: String - let reportStatus: String + let reportStatus: String? let reportCategory: String let reportImageUrl: String? let reportImageUrls: [String]? @@ -27,7 +27,7 @@ struct ReportDTO: Decodable { title: reportTitle, date: reportDate, type: ReportType(rawValue: reportCategory) ?? .transportation, - progress: ReportProgress(rawValue: reportStatus) ?? .received, + progress: ReportProgress(rawValue: reportStatus ?? "") ?? .received, content: reportContent, location: LocationEntity( longitude: longitude, @@ -43,7 +43,7 @@ struct ReportDTO: Decodable { title: reportTitle, date: date, type: ReportType(rawValue: reportCategory) ?? .transportation, - progress: ReportProgress(rawValue: reportStatus) ?? .received, + progress: ReportProgress(rawValue: reportStatus ?? "") ?? .received, content: reportContent, location: LocationEntity( longitude: longitude, diff --git a/Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift b/Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift new file mode 100644 index 00000000..91548cb1 --- /dev/null +++ b/Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift @@ -0,0 +1,68 @@ +// +// FilePresignedEndpoint.swift +// DataSource +// +// Created by 이동현 on 11/22/25. +// + +import Foundation + +enum FilePresignedEndpoint { + case fetchPresignedURL(presignedConditions: [FilePresignedConditaionDTO]) +} + +extension FilePresignedEndpoint: Endpoint { + var baseURL: String { + switch self { + case .fetchPresignedURL: + return AppProperties.baseURL + "/api/v2/files" + } + } + + var path: String { + switch self { + case .fetchPresignedURL: + return baseURL + "/presigned-urls" + } + } + + var method: HTTPMethod { + switch self { + case .fetchPresignedURL: .post + } + } + + var headers: [String : String] { + let headers: [String: String] = [ + "Content-Type": "application/json", + "accept": "*/*" + ] + return headers + } + + var queryParameters: [String : String] { + return [:] + } + + var bodyParameters: [String : Any] { + switch self { + case .fetchPresignedURL(let presignedConditions): + return [:] + } + } + + var isAuthorized: Bool { + return true + } + + var bodyType: EndpointBodyType { + return .rawData + } + + var bodyData: Data? { + switch self { + case .fetchPresignedURL(let presignedConditions): + return try? JSONEncoder().encode(presignedConditions) + } + } +} diff --git a/Projects/DataSource/Sources/Endpoint/ReportEndpoint.swift b/Projects/DataSource/Sources/Endpoint/ReportEndpoint.swift index f6d451fc..2a85b660 100644 --- a/Projects/DataSource/Sources/Endpoint/ReportEndpoint.swift +++ b/Projects/DataSource/Sources/Endpoint/ReportEndpoint.swift @@ -6,21 +6,19 @@ // enum ReportEndpoint { + case register(report: ReportDTO) case fetchReports case fetchReportDetail(reportId: Int) } extension ReportEndpoint: Endpoint { var baseURL: String { - switch self { - case .fetchReports, .fetchReportDetail: - return AppProperties.baseURL + "/api/v2/reports" - } + return AppProperties.baseURL + "/api/v2/reports" } var path: String { switch self { - case .fetchReports: + case .register, .fetchReports: return baseURL case .fetchReportDetail(let reportId): return "\(baseURL)/\(reportId)" @@ -29,8 +27,10 @@ extension ReportEndpoint: Endpoint { var method: HTTPMethod { switch self { + case.register: + return .post case .fetchReports, .fetchReportDetail: - .get + return .get } } @@ -47,7 +47,12 @@ extension ReportEndpoint: Endpoint { } var bodyParameters: [String : Any] { - return [:] + switch self { + case .register(let report): + return report.dictionary + case .fetchReports, .fetchReportDetail: + return [:] + } } var isAuthorized: Bool { diff --git a/Projects/DataSource/Sources/Endpoint/S3UploadEndpoint.swift b/Projects/DataSource/Sources/Endpoint/S3UploadEndpoint.swift new file mode 100644 index 00000000..fb7ff55e --- /dev/null +++ b/Projects/DataSource/Sources/Endpoint/S3UploadEndpoint.swift @@ -0,0 +1,63 @@ +// +// S3UploadEndpoint.swift +// DataSource +// +// Created by 이동현 on 11/22/25. +// + +import Foundation + +enum S3Endpoint { + case uploadImage(uploadURL: String, data: Data) +} + +extension S3Endpoint: Endpoint { + var baseURL: String { + return "" + } + + var path: String { + switch self { + case .uploadImage(let uploadURL, _): + return uploadURL + } + } + + var method: HTTPMethod { + switch self { + case .uploadImage: + return .put + } + } + + var headers: [String : String] { + switch self { + case .uploadImage: + return [:] + } + } + + var queryParameters: [String : String] { + + return [:] + } + + var bodyParameters: [String : Any] { + return [:] + } + + var isAuthorized: Bool { + return false + } + + var bodyType: EndpointBodyType { + return .rawData + } + + var bodyData: Data? { + switch self { + case .uploadImage(_, let data): + return data + } + } +} diff --git a/Projects/DataSource/Sources/Repository/FileRepository.swift b/Projects/DataSource/Sources/Repository/FileRepository.swift new file mode 100644 index 00000000..978b3940 --- /dev/null +++ b/Projects/DataSource/Sources/Repository/FileRepository.swift @@ -0,0 +1,25 @@ +// +// FileRepository.swift +// DataSource +// +// Created by 이동현 on 11/22/25. +// + +import Domain +import Foundation + +final class FileRepository: FileRepositoryProtocol { + private let networkService = NetworkService.shared + + func fetchPresignedURL(prefix: String?, fileNames: [String]) async throws -> [String : String]? { + let dtos = fileNames.map { FilePresignedConditaionDTO(prefix: prefix, fileName: $0) } + let endpoint = FilePresignedEndpoint.fetchPresignedURL(presignedConditions: dtos) + + return try await networkService.request(endpoint: endpoint, type: [String:String].self) + } + + func uploadFile(url: String, data: Data) async throws { + let endPoint = S3Endpoint.uploadImage(uploadURL: url, data: data) + _ = try await networkService.request(endpoint: endPoint, type: EmptyResponseDTO.self) + } +} diff --git a/Projects/DataSource/Sources/Repository/ReportRepository.swift b/Projects/DataSource/Sources/Repository/ReportRepository.swift index 010b2176..d154a6db 100644 --- a/Projects/DataSource/Sources/Repository/ReportRepository.swift +++ b/Projects/DataSource/Sources/Repository/ReportRepository.swift @@ -6,12 +6,34 @@ // import Domain +import Foundation final class ReportRepository: ReportRepositoryProtocol { private let networkService = NetworkService.shared - func report(reportEntity: ReportEntity) async { - + func report( + title: String, + content: String?, + category: ReportType, + location: LocationEntity?, + photoURLs: [String] + ) async throws { + let reportDTO = ReportDTO( + reportId: nil, + reportDate: nil, + reportTitle: title, + reportContent: content, + reportLocation: location?.address ?? "", + reportStatus: nil, + reportCategory: category.description, + reportImageUrl: nil, + reportImageUrls: photoURLs, + latitude: location?.latitude, + longitude: location?.longitude + ) + + let endpoint = ReportEndpoint.register(report: reportDTO) + guard let response = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self) else { return } } func fetchReports() async throws -> [ReportEntity] { diff --git a/Projects/Domain/Sources/DomainDependencyAssembler.swift b/Projects/Domain/Sources/DomainDependencyAssembler.swift index 3a18143d..8cc4def2 100644 --- a/Projects/Domain/Sources/DomainDependencyAssembler.swift +++ b/Projects/Domain/Sources/DomainDependencyAssembler.swift @@ -66,10 +66,14 @@ public struct DomainDependencyAssembler: DependencyAssemblerProtocol { DIContainer.shared.register(type: ReportUseCaseProtocol.self) { container in guard let locationRepository = container.resolve(type: LocationRepositoryProtocol.self), - let reportRepository = container.resolve(type: ReportRepositoryProtocol.self) + let reportRepository = container.resolve(type: ReportRepositoryProtocol.self), + let fileRepository = container.resolve(type: FileRepositoryProtocol.self) else { fatalError("reportUseCase에 필요한 의존성이 등록되지 않았습니다.") } - return ReportUseCase(locationRepository: locationRepository, reportRepository: reportRepository) + return ReportUseCase( + locationRepository: locationRepository, + reportRepository: reportRepository, + fileRepository: fileRepository) } } } diff --git a/Projects/Domain/Sources/Entity/Enum/ReportType.swift b/Projects/Domain/Sources/Entity/Enum/ReportType.swift index add8c316..11789c16 100644 --- a/Projects/Domain/Sources/Entity/Enum/ReportType.swift +++ b/Projects/Domain/Sources/Entity/Enum/ReportType.swift @@ -11,3 +11,9 @@ public enum ReportType: String, CaseIterable { case water case convenience } + +extension ReportType: CustomStringConvertible { + public var description: String { + return self.rawValue.uppercased() + } +} diff --git a/Projects/Domain/Sources/Protocol/Repository/FIileRepositoryProtocol.swift b/Projects/Domain/Sources/Protocol/Repository/FIileRepositoryProtocol.swift new file mode 100644 index 00000000..8f6eee1f --- /dev/null +++ b/Projects/Domain/Sources/Protocol/Repository/FIileRepositoryProtocol.swift @@ -0,0 +1,23 @@ +// +// FIileRepository.swift +// Domain +// +// Created by 이동현 on 11/22/25. +// + +import Foundation + +public protocol FileRepositoryProtocol { + /// 파일을 업로드할 presignedURL을 발급받습니다. + /// - Parameters: + /// - prefix: 파일을 저장할 경로 prefix + /// - fileNames: 업로드할 파일들의 이름. + /// - Returns: 업로드할 presignedURL (key: fileName, value: url) + func fetchPresignedURL(prefix: String?, fileNames: [String]) async throws -> [String: String]? + + /// 주어진 url로 파일을 업로드 합니다 + /// - Parameters: + /// - url: 파일을 업로드할 url + /// - data: 업로드할 data + func uploadFile(url: String, data: Data) async throws +} diff --git a/Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift b/Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift index 9e5d06cd..fb3c0443 100644 --- a/Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift +++ b/Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift @@ -5,8 +5,24 @@ // Created by 이동현 on 11/9/25. // +import Foundation + public protocol ReportRepositoryProtocol { - func report(reportEntity: ReportEntity) async + + /// 제보를 등록합니다. + /// - Parameters: + /// - title: 제보 제목 + /// - content: 제보 내용 + /// - category: 제보 카테고리 + /// - location: 제보 위치 + /// - photos: 업로드한 사진의 presigned urls + func report( + title: String, + content: String?, + category: ReportType, + location: LocationEntity?, + photoURLs: [String] + ) async throws /// 제보 목록을 조회합니다. /// - Returns: 조회된 제보 목록 diff --git a/Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift b/Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift index 62577b6c..48f77531 100644 --- a/Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift +++ b/Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift @@ -5,8 +5,20 @@ // Created by 이동현 on 11/9/25. // +import Foundation + public protocol ReportUseCaseProtocol { func fetchCurrentLocation() async throws -> LocationEntity? - func report(reportEntity: ReportEntity) async + func fetchReports() async throws -> [ReportEntity] + + func fetchReport(reportId: Int) async throws -> ReportEntity? + + func report( + title: String, + content: String?, + category: ReportType, + location: LocationEntity?, + photos: [Data] + ) async throws } diff --git a/Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift b/Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift index a5a11b63..3b9e18f7 100644 --- a/Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift +++ b/Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift @@ -5,13 +5,21 @@ // Created by 이동현 on 11/9/25. // +import Foundation + public final class ReportUseCase: ReportUseCaseProtocol { private let locationRepository: LocationRepositoryProtocol + private let fileRepository: FileRepositoryProtocol private let reportRepository: ReportRepositoryProtocol - public init(locationRepository: LocationRepositoryProtocol, reportRepository: ReportRepositoryProtocol) { + public init( + locationRepository: LocationRepositoryProtocol, + reportRepository: ReportRepositoryProtocol, + fileRepository: FileRepositoryProtocol + ) { self.locationRepository = locationRepository self.reportRepository = reportRepository + self.fileRepository = fileRepository } public func fetchCurrentLocation() async throws -> LocationEntity? { @@ -20,7 +28,50 @@ public final class ReportUseCase: ReportUseCaseProtocol { return try await locationRepository.fetchAddress(coordinate: coordinate) } - public func report(reportEntity: ReportEntity) async { + public func fetchReports() async throws -> [ReportEntity] { + return try await reportRepository.fetchReports() + } + + public func fetchReport(reportId: Int) async throws -> ReportEntity? { + return try await reportRepository.fetchReportDetail(reportId: reportId) + } + + public func report( + title: String, + content: String?, + category: ReportType, + location: LocationEntity?, + photos: [Data] + ) async throws { + let fileNames = (1...photos.count).map { "\($0).jpg" } + + // TODO: - 사진 업로드 실패 시 에러 처리 필요 + guard + let presignedDict = try await fileRepository.fetchPresignedURL(prefix: "report", fileNames: fileNames), + presignedDict.count == photos.count + else { return } + + let presignedURLs = Array(presignedDict.values) + + for (url, photo) in zip(presignedURLs, photos) { + do { + try await fileRepository.uploadFile(url: url, data: photo) + } catch { + print(error.localizedDescription) + } + } + + let publicImageURLs = presignedURLs.map { url in + url.split(separator: "?", maxSplits: 1) + .map(String.init) + .first ?? url + } + try await reportRepository.report( + title: title, + content: content, + category: category, + location: location, + photoURLs: publicImageURLs) } } diff --git a/Projects/Presentation/Sources/Home/View/HomeViewController.swift b/Projects/Presentation/Sources/Home/View/HomeViewController.swift index 6fcc022a..9cb45712 100644 --- a/Projects/Presentation/Sources/Home/View/HomeViewController.swift +++ b/Projects/Presentation/Sources/Home/View/HomeViewController.swift @@ -666,14 +666,14 @@ extension HomeViewController: RoutineViewDelegate { extension HomeViewController: FloatingMenuViewDelegate { func floatingMenuDidTapReportButton(_ sender: FloatingMenuView) { toggleFloatingButton() - // TODO: 제보하기 뷰로 이동 (현재는 제보 detailView) - guard let reportDetailViewModel = DIContainer.shared.resolve(type: ReportDetailViewModel.self) - else { fatalError("reportDetailViewModel 의존성이 등록되지 않았습니다.") } - let reportDetailViewController = ReportDetailViewController(viewModel: reportDetailViewModel, reportId: 1) - reportDetailViewController.hidesBottomBarWhenPushed = true + guard let reportRegistrationViewModel = DIContainer.shared.resolve(type: ReportRegistrationViewModel.self) + else { fatalError("reportRegistrationViewController 의존성이 등록되지 않았습니다.") } - self.navigationController?.pushViewController(reportDetailViewController, animated: true) + let reportRegistrationViewController = ReportRegistrationViewController(viewModel: reportRegistrationViewModel) + reportRegistrationViewController.hidesBottomBarWhenPushed = true + + self.navigationController?.pushViewController(reportRegistrationViewController, animated: true) } func floatingMenuDidTapRegisterRoutineButton(_ sender: FloatingMenuView) { diff --git a/Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift b/Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift index 4e04628b..673e300b 100644 --- a/Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift +++ b/Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift @@ -83,3 +83,13 @@ final class ReportLoadingViewController: UIViewController { } } } + +extension ReportLoadingViewController: ReportRegistrationViewControllerDelegate { + func reportRegistrationViewController(_ sender: ReportRegistrationViewController, completeRegistration: Bool) { + if completeRegistration { + navigationController?.popToRootViewController(animated: true) + } else { + navigationController?.popViewController(animated: true) + } + } +} diff --git a/Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift b/Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift index 92f877fa..4d2f9eae 100644 --- a/Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift +++ b/Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift @@ -11,6 +11,10 @@ import PhotosUI import SnapKit import UIKit +protocol ReportRegistrationViewControllerDelegate: AnyObject { + func reportRegistrationViewController(_ sender: ReportRegistrationViewController, completeRegistration: Bool) +} + final class ReportRegistrationViewController: BaseViewController { private enum CollectionViewSection { case main } @@ -63,6 +67,7 @@ final class ReportRegistrationViewController: BaseViewController(items: SelectPhotoType.allCases.sorted(by: { $0.id < $1.id }), markIsSelected: false) private var cancellables: Set = [] private var dataSource: DataSource? + weak var delegate: ReportRegistrationViewControllerDelegate? override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) @@ -108,6 +113,9 @@ final class ReportRegistrationViewController: BaseViewController let locationPublisher: AnyPublisher let selectedPhotoPublisher: AnyPublisher<[PhotoItem], Never> + let isReportValid: AnyPublisher let exceptionPublisher: AnyPublisher + let reportRegistrationCompletePublisher: AnyPublisher let maxPhotoCount: Int } @@ -37,6 +39,8 @@ final class ReportRegistrationViewModel: ViewModel { private let contentSubject = CurrentValueSubject(nil) private let locationSubject = CurrentValueSubject(nil) private let selectedPhotoSubject = CurrentValueSubject<[PhotoItem], Never>([]) + private let reportVerificationSubject = PassthroughSubject() + private let reportRegistrationCompleteSubject = PassthroughSubject() private let exceptionSubject = PassthroughSubject() private let maxPhotoCount = 3 private var location: LocationEntity? = nil @@ -51,7 +55,9 @@ final class ReportRegistrationViewModel: ViewModel { contentPublisher: contentSubject.eraseToAnyPublisher(), locationPublisher: locationSubject.eraseToAnyPublisher(), selectedPhotoPublisher: selectedPhotoSubject.eraseToAnyPublisher(), + isReportValid: reportVerificationSubject.eraseToAnyPublisher(), exceptionPublisher: exceptionSubject.eraseToAnyPublisher(), + reportRegistrationCompletePublisher: reportRegistrationCompleteSubject.eraseToAnyPublisher(), maxPhotoCount: maxPhotoCount) } @@ -77,25 +83,29 @@ final class ReportRegistrationViewModel: ViewModel { private func configureCategory(type: ReportType?) { categorySubject.send(type) selectedReportType = type + verifyIsReportValid() } private func configureTitle(title: String?) { titleSubject.send(title) + verifyIsReportValid() } private func configureContent(content: String?) { contentSubject.send(content) + verifyIsReportValid() } private func configureLocation() { Task { do { self.location = try await reportUseCase.fetchCurrentLocation() + locationSubject.send(location?.address) + verifyIsReportValid() } catch { - + locationSubject.send(nil) + verifyIsReportValid() } - - locationSubject.send(location?.address) } } @@ -112,14 +122,72 @@ final class ReportRegistrationViewModel: ViewModel { currentSelectedPhoto.append(item) selectedPhotoSubject.send(currentSelectedPhoto) + verifyIsReportValid() } private func removePhoto(id: UUID) { let currentSelectedPhoto = selectedPhotoSubject.value.filter { $0.id != id } selectedPhotoSubject.send(currentSelectedPhoto) + verifyIsReportValid() } private func register() { + guard + let name = titleSubject.value, + !name.isEmpty, + let category = categorySubject.value, + let content = contentSubject.value, + let location, + selectedPhotoSubject.value.count > 0 + else { return } + + let selectedPhotos = selectedPhotoSubject.value.map({ $0.data }) + + Task { + let minimumDuration: TimeInterval = 0.7 + let startTime = Date() + let result: Bool + + do { + try await reportUseCase.report( + title: name, + content: content, + category: category, + location: location, + photos: selectedPhotos) + result = true + } catch { + + result = false + } + + let elapsed = Date().timeIntervalSince(startTime) + if elapsed < minimumDuration { + let remaining = minimumDuration - elapsed + try? await Task.sleep( + nanoseconds: UInt64(remaining * 1_000_000_000) + ) + } + + reportRegistrationCompleteSubject.send(result) + } + } + + private func verifyIsReportValid() { + guard + let name = titleSubject.value, + !name.isEmpty, + categorySubject.value != nil, + contentSubject.value != nil, + let location, + location.latitude != nil, + location.longitude != nil, + selectedPhotoSubject.value.count > 0 + else { + reportVerificationSubject.send(false) + return + } + reportVerificationSubject.send(true) } } From c00b6526e61ccbcac7da880defc533684fb6c058 Mon Sep 17 00:00:00 2001 From: taipaise Date: Sat, 22 Nov 2025 17:11:05 +0900 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20=EC=A0=9C=EB=B3=B4=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=88=98=EC=A0=95=EB=90=9C=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Contents.json | 23 +++++++ .../bitnagil_camera_icon.png | Bin 0 -> 587 bytes .../bitnagil_camera_icon@2x.png | Bin 0 -> 1036 bytes .../bitnagil_camera_icon@3x.png | Bin 0 -> 1522 bytes .../Contents.json | 23 +++++++ .../bitnagil_photo_icon.png | Bin 0 -> 672 bytes .../bitnagil_photo_icon@2x.png | Bin 0 -> 1118 bytes .../bitnagil_photo_icon@3x.png | Bin 0 -> 1772 bytes .../Common/Component/SelectableItemCell.swift | 29 ++++++++- .../Common/DesignSystem/BitnagilIcon.swift | 2 + .../Common/Protocol/SelectableItem.swift | 7 +++ .../Report/Model/SelectPhotoType.swift | 11 ++++ .../ReportRegistration/ReportTextView.swift | 7 ++- .../View/ReportLoadingViewController.swift | 8 +-- .../ReportRegistrationViewController.swift | 57 +++++++++++++++--- 15 files changed, 146 insertions(+), 21 deletions(-) create mode 100644 Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/Contents.json create mode 100644 Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/bitnagil_camera_icon.png create mode 100644 Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/bitnagil_camera_icon@2x.png create mode 100644 Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/bitnagil_camera_icon@3x.png create mode 100644 Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/Contents.json create mode 100644 Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/bitnagil_photo_icon.png create mode 100644 Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/bitnagil_photo_icon@2x.png create mode 100644 Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/bitnagil_photo_icon@3x.png diff --git a/Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/Contents.json new file mode 100644 index 00000000..86ace5ec --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "bitnagil_camera_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bitnagil_camera_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "bitnagil_camera_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/bitnagil_camera_icon.png b/Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/bitnagil_camera_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..90429a620275b0513f56168ac4c34e4e9f19cddf GIT binary patch literal 587 zcmV-R0<`^!P)t~}8M5Z{N29&b5bRDLjLoP~rwv1^Rmdn1!d2c~2 zsU$}Nz6y6z;+iTj8X%(4j5D|_uO%FEIS zFkN$Vuki2hAN?s73)9^DB@4kEFgdn4KmQKJLIJ;4exu!PW9j=h)N1Rf1$7fD8fM73 zG|txJM-TCTbq#xa`)IX7tSIp8>=R6q2KK;Q)240badOfg+O06uW!ttr8AwQz4h|0S zqWo-VzkTysm0U-++Z_k-Q$1q3hYZ{`0k^hy@af~Cn$hQ&nYpJy?>{VS4v7Ne6NZ0@X002ovPDHLkV1oSB1DyZ> literal 0 HcmV?d00001 diff --git a/Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/bitnagil_camera_icon@2x.png b/Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/bitnagil_camera_icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..39917f90f531fe4c7af36640f5a6e452841dae50 GIT binary patch literal 1036 zcmV+n1oQieP)gfv5Fh2J@s$`wJ@|-^L`@V=8jTTd97qHq>dhjU zl-5U2MNR^qse81Z_5jily5oFX3f+Y%GYbiq{*q>QUhRLrZ@zD43RI|2p)3sRAR3K^ za=G~^y>lLD5RZ`fOftqN48yi;yUL2kVOaG*H3LHmGiBCiU|pl5!?(TlD_m^z{CBxH!ml`IY*^X?ek&3=-t4V+ z38d)syS_F8;qsf_I+sA^V-as8TqFsv6+JHICN>!k<_f@IX~BssnuibC+fU-$*)z_2 zI{gC!gYS{eW}ywYw?Zb+W_gQo_0+gy$9CMka|`wLb*}oBmLpExz55TfMT#9~qO|N@ zgFJ8w8oYe*Y)M=;`xiYuy*Pj25;B>LBmQlFudwQAov##vC>1RebT_L?{Fe4OQhs zF)t1bzEe&&blQPjprO15@7;gkh?Kdn80CrNS0u$p74MnRn`WX=gv3owO^H-;6z$Pd zsIRL-YwJ;`t<<7pV`I>waA4?^2-G8){Dy-~jZU)m?j{7KnRI#_nz)vkdoKo3T;oJT z0w_t7xZS&V32QAUWN&U%g)yWc6DXR8bRwA`SL>{?wJ^l(&s8ohaJHV)^2Sg$ibWxX zO+?&F%ckWFYLAyo-%mKQ=iCO)DC$fr&|6oio zV@UJ~t?VPlGWMZxlfxdY1;f+C=;!Avt0mAs2*ueH?+Il7TrM9Kk$y?$2Y zS5~o`hKqfd#~130_u-nFKp)lTa)n7%SHtpJu0n+hiu?x#fnjFRTl%yB0000sB9TZgi8xd;F$GjX;$xeWQL!CDXzXKALW}qVg47>UxwNimK`oUE2q{Qa zlh6pb#cGP`CZM_p?Dpm)94gTS_vl_QQUhsX?=Wwjb#3qLX6qf(SlW-|tUa@v-S50N zZ{Ex-fQyTZi;IiP^8%rX3=KtnjFqDN>j3{O0kBa97(h@9{MK3i$uS0Z=v+l9bpf=e zlF394nn+UwN2AfMfBr2^ae+~IRv4UFShx{~CgB0ekX1LgN*Vs-`=JpKf`0#Nuy^mi zhYudyFMwpETR0LKxy=BAAOQrNM)bsGy14=p(fX_;u>=Ell(FIzNJN@o605S(ZIBE# z;PCKp03;zza5)+?A2b0~Rk}bDZO|ZOQGlY)>ut*>5((+SrHVo_c_USQPyX0BuNMl9 z+;_*I6^R_0;BCBZJey1>tSoBZT#472)v6=~fdyh5rk&V1&P^iDW<`!{LL_+tUMsg9o6ux2IA!IJjT?EOOCbUb+mD5oTdR z!N}k*8G8NxKDc<{ye8U;Q0Sm`yf*tIhk(T?f#T}?D`iMuhCjgKAHe$)A5V;{id$LlbcxDHN0R1%_cgWyuB zt$<1qoY2?TSJPKdocz?(RcB{^(CWsyQ2m-%Y}Bz}uh%0KY#5fdwjjsb$I*0;+)`(G zd1c3bNkQwx$x|Sc&CN}psN5hpmEv;{Qg%e-^o4gpu0IimhSI=3!Jc}dW5Sa zP06&Xqw?z7nyqW7VaOYFjvxDE$LMZ_!c<|<6$@rK01l|NRZltL7ic8I!9q2v*gS;}yc{{b`jV*o5WzG>pZf zqm|!tbJrcGn0$FECn`6I($2QH3pRG<*DD%3;lM$yNW5QN8W?E1R@;ubB zTa#$AvL1wJfwS3_Eer8((mewj7X<5HC@61f$+QUM;C6+*JNDA*%D*Ywlzq! zZe8lj^@zsV<&_nX1*$B_4>f|Dy2La2-4)G6tdUX?cNbw;>daCJSJ#ga=W$rX?1PI0G#V-#X3RtQ&uI zgo2aF`DqkHxK@F$Sem}}WI7H%dD0d`lYDen!^M)p%&JY_)5_)UZ|vE#Z^rWidl@Rb z{gh*4l7D%l?k?2ly@q=u_Y#B|{)Ir}`LE0{kfl_!cOwwJp8qZPT_W=y}R#7JanR%Cpp5lixLQadB~RadC0T Ye}>r$Y!xkn$p8QV07*qoM6N<$f&gRdQ~&?~ literal 0 HcmV?d00001 diff --git a/Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/Contents.json new file mode 100644 index 00000000..11c8b6f1 --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "bitnagil_photo_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bitnagil_photo_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "bitnagil_photo_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/bitnagil_photo_icon.png b/Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/bitnagil_photo_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..46fbebe9c8857d9d7fdef97dd2601d92adb62912 GIT binary patch literal 672 zcmV;R0$=@!P){ z&VYLySP}*Lh=DIH9%J0+oSC$T>;i=uW85hgi*5_y^z`f>Q77v`)#m#8uSqSWuB=xw z0-Ys}kT3wA?Dnu_S#9kBV3Nj|{x!6p4p{!U1jEBOpj0Zuv*#}&YfNx6REQk>PGm$x zA0d^p)z=6_=7)rgiH?Njav3U>Lp7(7R7MoCKxJBm%sHBX=Z9Ra*5J+CcaX{4gpG~g zE$c_z7?Fdk6bcrY%^MPl1dNT1Mv_3vR#twwt&2iqR1t2;y3zNV&bf0OU$A@l$Q^bn27wKd0UYOr@6 z2>~XeaH)w>lU3me!=xrl4Od5`?HbJM>x;RL<8*w!J>dl0>cX`ID#$wk0000_>xWc&3iL^yqWhi3vh`46n4A4 zfu3S^Oiy964O%&eDGsa?fTtH?F?q*!X4Aj@HVQEcda1Lua*dJALB#F3M-4%+*a zbs4b7XzTZQ-nmL5XtUW(NuF56$tI{MSXg2;y>7QFER3p!>XFOP2_HLJbK2!{#gq|A ztxiC~T0L?EZra}dJE|TL2r>>sP;D^Dq83GhnP8|U#2u^^RH-NhBhk@u3hL_WAQbuu zkw^q~3xObc&?tIMO^q;hXA;cj7Fmu&qA+v!9t4A-!V@(FB#q7r29bL-`yfM38Jm50 zU-o7Zepb*Uh*$>$K-)@$w5YGIgPxwQf^(UMgQU?eg8}I1Xoo$(uNE|0oD72j@OnS$ z>H>jpPzXuJ^gYNxGJydI1ir${rPn#*b92v&MzX|{LaM7#3ubfc1pc&v0U(kTxfCc2 z57zwrBD{4KKS5}^6Ab=%qsaS~W(Z#I2l?3*7(1>OB{*QeEtCP;ww9I?(9_+8 z6(W@Pyp@jvrLGiEesf+OlXD$`evKrE=hdf{#Wcg%CKLe?Qpgb~`Hl=<2E)K%1M~=D zyQN!6hzQlzYtHc;o!vepl(juF41*p)&h*=@#>OVOOy#Sk3VGW4Vzss?J;oYz2^PYQ z>k|d_bT%E1A?WYBpld^3b~rAfZIiAI=E3uzN04zA(@r3dOixXgyoE$EpBIPmoIZQ{ z1id_tolb$ShV$RL1W}vQ@)No4?!q028stfuqa`;od_}O)Cn;ZBGY~s!iejFvB~Is< z5V56#<}5C}0FU=QSTs?ufOf=UX@|A7&%(I2SrBURNpmZCuu~!Du$O;_qQJ4peWCt& zJ9qXBw6(Q@-~TByCgHUs82rARl~qXH{woLCV#*BaJX)lq&Ze1odg_P=s}ZOr)gNTb&%R5>gS9Ek)So{Q3%od(kr2s3yE6n2N_%7oLiNN zH*ejRBT!kC(u4LgM8OdG6@_^E57y&WMWRu8%la^Blb((X{ljWc5h#G-@whOiy)@-~ z-5MAeTs7+YSr!<~c-$+~Suqd#2L``DI^9$h8ghiZ{otRS+TJ5a5~KJ!!Bc0#htai( zpGKM??Op8KT-01*UT;Xtv6ReWjFK$}E6+5aG?t8UF0GS|tWVoweciy1n9aEUg4?~~ kgGz$FHZMSNRfHIQP!n zIdgx`x#!#kSYwSfN*U&+^3I(*>kR|y4N$`7#hh*8jNetjNB}5_zP`(`Y@O^ihM^`E zMUD6MX^EDWsJJd24hDU3P1BOO+KNV_p;T%vnr$m{GZrr^Q)9EU=}yL2*kJIx3Th1R z4GwXna7)W8O$KLY0C+gh?Sv9Me{dQ~|9pKogWWjKd3* zTvE8HDH_VZ8W%ryohB(93_^*l$oa|G{lzHoNRtj*B|fVlm4c){0INcIc{wyTZin^j zpMsg085kVA4wI9UP$-IGNUJnymYQE~o$}h*sIA=!M-Cq}m0ifWb3eh4=YEDl5T>cp zYMLa+Bja81cJPpfhV5{?y9+8R%B3=V5!frYZ~qN$nbY~5@zIg^LYX{=6f<1cl z$ibX-+S=YS6?Y3Gtc|2FH+MeX@wQL~=WaeC)m7jYm9e;0CMkSB<<~Mh8*gyhbJdY* za7i){P-9l1q;M=|PVyKO88g$~JRWJ($e#p)a5 z=Ac^m!KF1iK!o}(8iZ`zj?r(wA%>O&JS-dVGQDw zoc6SUla(oOVLB`#6{k*~$XPEF%~%iKA<`}cq{0ROnWmuVcyGTsWb^H5aRlIy_e^Qd z|D(i;jj#?@095+a$sRcQbV>Cg9(%9JGD71qCJ{%P|9e()!JF{JW1oGRcX(rnhmuP@jN3yIxg_JJIK0rjEGOHxJuk(b zJ398m>#x0PdkrcbiB!SBz*XBi!n9r=8~f+HEGM8c51AGAgrj*^2phiV<$**(-g$`>b+u4o5a`=>T}R!r`jg{YBTJxwPD1uWj7;H2D2~$JbyU3g`6j z@CX?w7#kZoZIwxL7*Fh2^TRmLtT~@z0G|n!K61E|Dz4AyvKTq@z~y*We~!6Q3}7!) z=1K<^w#sCMS*aXRS`6i@`xrP2#FrvC3Z3MVC50LHF0ZtlP=UWRci>twCT|V#X5-_< z5u%~UL^_@2j~&}#G#lv8R##^Nb7(h*L_-_IN{MDGpY24mfmwzu*<%ej3{g&PLa=;j zK5RQVaSx{FO;3ert3j;J*fz?#y1L0W#hzfF_539rmu9?2_?L?gr{O~#c)#<(!a&Uz zEVIh9kxMG3g)b>}{z+8_s&%{UsoXp~rr-X^=DO=~;clok5VS6`lh?!M{ zbi2sAhD0Np^%%c-+)3f}_;r9_(63=x<`{o^{zdr`_}eh_dPUJgy6%swDsIy8s{yU; zA_ny^pKAu-u&4AS-YXNTNry2>FfgFS@xEfM3yU=`k?*SnYpk)xqs9L(1$^SObCCN0 O0000(items: SelectPhotoType.allCases.sorted(by: { $0.id < $1.id }), markIsSelected: false) @@ -120,6 +124,9 @@ final class ReportRegistrationViewController: BaseViewController Date: Sat, 22 Nov 2025 18:12:59 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=EC=A0=9C=EB=B3=B4=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=ED=95=84=ED=84=B0=EB=A7=81=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataSource/Sources/DTO/ReportDTO.swift | 2 ++ .../Domain/Sources/Entity/ReportEntity.swift | 8 ++++--- .../ReportHistoryTableViewCell.swift | 2 ++ .../View/ReportCompleteViewController.swift | 6 ++++++ .../View/ReportHistoryViewController.swift | 4 ++-- .../ViewModel/ReportDetailViewModel.swift | 2 +- .../ViewModel/ReportHistoryViewModel.swift | 21 ++++++++++++------- Projects/Shared/Sources/Extension/Date+.swift | 2 ++ 8 files changed, 33 insertions(+), 14 deletions(-) diff --git a/Projects/DataSource/Sources/DTO/ReportDTO.swift b/Projects/DataSource/Sources/DTO/ReportDTO.swift index e74588b0..3fd7c92f 100644 --- a/Projects/DataSource/Sources/DTO/ReportDTO.swift +++ b/Projects/DataSource/Sources/DTO/ReportDTO.swift @@ -33,6 +33,7 @@ struct ReportDTO: Codable { longitude: longitude, latitude: latitude, address: reportLocation), + thumbnailURL: reportImageUrl, photoUrls: reportImageUrls ?? []) } @@ -49,6 +50,7 @@ struct ReportDTO: Codable { longitude: longitude, latitude: latitude, address: reportLocation), + thumbnailURL: reportImageUrl, photoUrls: reportImageUrls ?? []) } } diff --git a/Projects/Domain/Sources/Entity/ReportEntity.swift b/Projects/Domain/Sources/Entity/ReportEntity.swift index 18b9c4c7..531cf269 100644 --- a/Projects/Domain/Sources/Entity/ReportEntity.swift +++ b/Projects/Domain/Sources/Entity/ReportEntity.swift @@ -13,7 +13,8 @@ public struct ReportEntity { public let progress: ReportProgress public let content: String? public let location: LocationEntity - public let photoUrls: [String] + public let thumbnailURL: String? + public let photoURLs: [String] public init( id: Int, @@ -23,6 +24,7 @@ public struct ReportEntity { progress: ReportProgress, content: String?, location: LocationEntity, + thumbnailURL: String?, photoUrls: [String] ) { self.id = id @@ -32,7 +34,7 @@ public struct ReportEntity { self.progress = progress self.content = content self.location = location - self.photoUrls = photoUrls + self.thumbnailURL = thumbnailURL + self.photoURLs = photoUrls } - } diff --git a/Projects/Presentation/Sources/Report/View/Component/ReportHistory/ReportHistoryTableViewCell.swift b/Projects/Presentation/Sources/Report/View/Component/ReportHistory/ReportHistoryTableViewCell.swift index 5d50f045..1e93b2f7 100644 --- a/Projects/Presentation/Sources/Report/View/Component/ReportHistory/ReportHistoryTableViewCell.swift +++ b/Projects/Presentation/Sources/Report/View/Component/ReportHistory/ReportHistoryTableViewCell.swift @@ -46,6 +46,8 @@ final class ReportHistoryTableViewCell: UITableViewCell { private func configureAttribute() { backgroundColor = .clear + selectionStyle = .none + containerView.backgroundColor = .white containerView.layer.cornerRadius = Layout.containerViewCornerRadius containerView.layer.masksToBounds = true diff --git a/Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift b/Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift index 9da93582..953d877e 100644 --- a/Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift +++ b/Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift @@ -103,6 +103,12 @@ final class ReportCompleteViewController: UIViewController { photoStackView.axis = .horizontal photoStackView.spacing = Layout.photoStackViewSpacing + + confirmButton.addAction( + UIAction { [weak self] _ in + self?.navigationController?.popToRootViewController(animated: true) + }, + for: .touchUpInside) } private func configureLayout() { diff --git a/Projects/Presentation/Sources/Report/View/ReportHistoryViewController.swift b/Projects/Presentation/Sources/Report/View/ReportHistoryViewController.swift index 25eb2f7f..1a8760ca 100644 --- a/Projects/Presentation/Sources/Report/View/ReportHistoryViewController.swift +++ b/Projects/Presentation/Sources/Report/View/ReportHistoryViewController.swift @@ -267,7 +267,7 @@ final class ReportHistoryViewController: BaseViewController) var snapshot = NSDiffableDataSourceSnapshot() @@ -279,7 +279,7 @@ final class ReportHistoryViewController: BaseViewController - let selectedProgressPublisher: AnyPublisher let categoryPublisher: AnyPublisher<[ReportType], Never> let selectedCategoryPublisher: AnyPublisher let reportsPublisher: AnyPublisher<[ReportHistoryItem], Never> @@ -27,12 +26,12 @@ final class ReportHistoryViewModel: ViewModel { private(set) var output: Output private let progressSubject = CurrentValueSubject<[ReportProgressItem], Never>([]) - private let selectedProgressSubject = CurrentValueSubject(nil) private let categorySubject = CurrentValueSubject<[ReportType], Never>([]) private let selectedCategorySubject = CurrentValueSubject(nil) private let reportSubject = CurrentValueSubject<[ReportHistoryItem], Never>([]) private let selectedReportSubject = PassthroughSubject() private(set) var selectedReportCategory: ReportType? + private var selectedProgress: ReportProgress? private var reports: [ReportHistoryItem] = [] private let reportRepository: ReportRepositoryProtocol @@ -49,7 +48,6 @@ final class ReportHistoryViewModel: ViewModel { output = Output( progressPublisher: progressSubject.eraseToAnyPublisher(), - selectedProgressPublisher: selectedProgressSubject.eraseToAnyPublisher(), categoryPublisher: categorySubject.eraseToAnyPublisher(), selectedCategoryPublisher: selectedCategorySubject.eraseToAnyPublisher(), reportsPublisher: reportSubject.eraseToAnyPublisher(), @@ -89,25 +87,29 @@ final class ReportHistoryViewModel: ViewModel { for i in 0.. Date: Sat, 22 Nov 2025 18:37:46 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20=EC=A0=9C=EB=B3=B4=ED=9E=88?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=20->=20=EC=A0=9C=EB=B3=B4=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EB=AF=B8=EB=B9=84?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 진행상황 collectionView cell에 진행상황 별 갯수 표시 - ReportEntity id 값을 옵셔널로 변경 - ReportDetail 날짜 포멧 변경 --- .../DataSource/Sources/DTO/ReportDTO.swift | 1 - .../Domain/Sources/Entity/ReportEntity.swift | 4 ++-- .../View/ReportHistoryViewController.swift | 1 - .../ViewModel/ReportDetailViewModel.swift | 7 ++++-- .../ViewModel/ReportHistoryViewModel.swift | 23 ++++++++++++++++++- Projects/Shared/Sources/Extension/Date+.swift | 2 ++ 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Projects/DataSource/Sources/DTO/ReportDTO.swift b/Projects/DataSource/Sources/DTO/ReportDTO.swift index 3fd7c92f..fc6d1a68 100644 --- a/Projects/DataSource/Sources/DTO/ReportDTO.swift +++ b/Projects/DataSource/Sources/DTO/ReportDTO.swift @@ -21,7 +21,6 @@ struct ReportDTO: Codable { let longitude: Double? func toReportEntity() throws -> ReportEntity { - guard let reportId else { throw NetworkError.decodingError } return ReportEntity( id: reportId, title: reportTitle, diff --git a/Projects/Domain/Sources/Entity/ReportEntity.swift b/Projects/Domain/Sources/Entity/ReportEntity.swift index 531cf269..bd5f79c5 100644 --- a/Projects/Domain/Sources/Entity/ReportEntity.swift +++ b/Projects/Domain/Sources/Entity/ReportEntity.swift @@ -6,7 +6,7 @@ // public struct ReportEntity { - public let id: Int + public let id: Int? public let title: String public let date: String? public let type: ReportType @@ -17,7 +17,7 @@ public struct ReportEntity { public let photoURLs: [String] public init( - id: Int, + id: Int?, title: String, date: String?, type: ReportType, diff --git a/Projects/Presentation/Sources/Report/View/ReportHistoryViewController.swift b/Projects/Presentation/Sources/Report/View/ReportHistoryViewController.swift index 1a8760ca..072a077b 100644 --- a/Projects/Presentation/Sources/Report/View/ReportHistoryViewController.swift +++ b/Projects/Presentation/Sources/Report/View/ReportHistoryViewController.swift @@ -346,5 +346,4 @@ extension ReportHistoryViewController: ReportCategoryTableViewControllerDelegate viewModel.action(input: .filterCategory(type: selectedCategory)) } - } diff --git a/Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift b/Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift index 728af5e8..4cf8ffed 100644 --- a/Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift +++ b/Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift @@ -7,6 +7,7 @@ import Combine import Domain +import Foundation final class ReportDetailViewModel: ViewModel { enum Input { @@ -39,8 +40,11 @@ final class ReportDetailViewModel: ViewModel { Task { do { if let reportEntity = try await reportRepository.fetchReportDetail(reportId: reportId) { + let date = Date.convertToDate(from: reportEntity.date ?? "", dateType: .yearMonthDate) + let dateString = date?.convertToString(dateType: .yearMonthDateWeek2) + let reportDetail = ReportDetail( - date: reportEntity.date ?? "", + date: dateString ?? "", title: reportEntity.title, status: reportEntity.progress, category: reportEntity.type, @@ -49,7 +53,6 @@ final class ReportDetailViewModel: ViewModel { photoUrls: reportEntity.photoURLs) reportDetailSubject.send(reportDetail) } - reportDetailSubject.send(nil) } catch { reportDetailSubject.send(nil) } diff --git a/Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift b/Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift index 2bd3a967..e5480c5b 100644 --- a/Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift +++ b/Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift @@ -126,8 +126,10 @@ final class ReportHistoryViewModel: ViewModel { let date = Date.convertToDate(from: reportEntity.date ?? "", dateType: .yearMonthDate) let dateString = date?.convertToString(dateType: .yearMonthDateWeek) + guard let id = reportEntity.id else { continue } + let reportHistoryItem = ReportHistoryItem( - id: reportEntity.id, + id: id, title: reportEntity.title, thumbnailUrl: reportEntity.thumbnailURL ?? "", date: dateString ?? "", @@ -139,6 +141,25 @@ final class ReportHistoryViewModel: ViewModel { reports = reportHistoryItems reportSubject.send(reportHistoryItems) + + let progressItems: [ReportProgressItem] = ReportProgress.allCases.map { progress in + let count: Int + + switch progress { + case .entire: + count = reports.count + default: + count = reports.filter { $0.progress == progress }.count + } + + return ReportProgressItem( + uuid: UUID(), + progress: progress, + count: count, + isSelected: progress == .entire) + } + + progressSubject.send(progressItems) } catch { // TODO: 에러 처리 } diff --git a/Projects/Shared/Sources/Extension/Date+.swift b/Projects/Shared/Sources/Extension/Date+.swift index 12379921..c5881dd6 100644 --- a/Projects/Shared/Sources/Extension/Date+.swift +++ b/Projects/Shared/Sources/Extension/Date+.swift @@ -33,6 +33,7 @@ extension Date { case yearMonthDate case yearMonthDateShort case yearMonthDateWeek + case yearMonthDateWeek2 case yearMonth case dayOfWeek case date @@ -46,6 +47,7 @@ extension Date { case .yearMonthDate: "yyyy-MM-dd" case .yearMonthDateShort: "yy.MM.dd" case .yearMonthDateWeek: "yyyy-MM-dd E" + case .yearMonthDateWeek2: "yyyy-MM-dd (E)" case .yearMonth: "yyyy년 M월" case .dayOfWeek: "E" case .date: "d" From 4239784088636772673b8afa3e2610892fa39706 Mon Sep 17 00:00:00 2001 From: taipaise Date: Sat, 22 Nov 2025 19:37:10 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=EC=A0=9C=EB=B3=B4=ED=95=98=EA=B8=B0?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Repository/ReportRepository.swift | 6 ++++-- .../Repository/ReportRepositoryProtocol.swift | 3 ++- .../UseCase/ReportUseCaseProtocol.swift | 17 ++++++++++++++++- .../Sources/UseCase/Report/ReportUseCase.swift | 6 +++--- .../View/ReportLoadingViewController.swift | 5 +++-- .../View/ReportRegistrationViewController.swift | 11 ++++------- .../ViewModel/ReportRegistrationViewModel.swift | 14 ++++++-------- 7 files changed, 38 insertions(+), 24 deletions(-) diff --git a/Projects/DataSource/Sources/Repository/ReportRepository.swift b/Projects/DataSource/Sources/Repository/ReportRepository.swift index d154a6db..c7205bbb 100644 --- a/Projects/DataSource/Sources/Repository/ReportRepository.swift +++ b/Projects/DataSource/Sources/Repository/ReportRepository.swift @@ -17,7 +17,7 @@ final class ReportRepository: ReportRepositoryProtocol { category: ReportType, location: LocationEntity?, photoURLs: [String] - ) async throws { + ) async throws -> Int? { let reportDTO = ReportDTO( reportId: nil, reportDate: nil, @@ -33,7 +33,9 @@ final class ReportRepository: ReportRepositoryProtocol { ) let endpoint = ReportEndpoint.register(report: reportDTO) - guard let response = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self) else { return } + guard let id = try await networkService.request(endpoint: endpoint, type: Int.self) else { return nil } + + return id } func fetchReports() async throws -> [ReportEntity] { diff --git a/Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift b/Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift index fb3c0443..6fca620d 100644 --- a/Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift +++ b/Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift @@ -16,13 +16,14 @@ public protocol ReportRepositoryProtocol { /// - category: 제보 카테고리 /// - location: 제보 위치 /// - photos: 업로드한 사진의 presigned urls + /// - Returns: 제보 id func report( title: String, content: String?, category: ReportType, location: LocationEntity?, photoURLs: [String] - ) async throws + ) async throws -> Int? /// 제보 목록을 조회합니다. /// - Returns: 조회된 제보 목록 diff --git a/Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift b/Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift index 48f77531..a8e83028 100644 --- a/Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift +++ b/Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift @@ -8,17 +8,32 @@ import Foundation public protocol ReportUseCaseProtocol { + /// 현위치를 가져옵니다. + /// - Returns: 현재 위치 func fetchCurrentLocation() async throws -> LocationEntity? + /// 제보 목록을 가져옵니다. + /// - Returns: 제보 목록 func fetchReports() async throws -> [ReportEntity] + /// 제보 단건의 상세 정보를 조회합니다. + /// - Parameter reportId: 제보 id + /// - Returns: 제보 상세 정보 func fetchReport(reportId: Int) async throws -> ReportEntity? + /// 제보를 등록합니다. + /// - Parameters: + /// - title: 제보 제목 + /// - content: 제보 내용 + /// - category: 제보 카테고리 (교통, 상하수도 등) + /// - location: 제보 위치 + /// - photos: 제보 사진 배열 + /// - Returns: 등록한 제보 id func report( title: String, content: String?, category: ReportType, location: LocationEntity?, photos: [Data] - ) async throws + ) async throws -> Int? } diff --git a/Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift b/Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift index 3b9e18f7..b57bcb9b 100644 --- a/Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift +++ b/Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift @@ -42,14 +42,14 @@ public final class ReportUseCase: ReportUseCaseProtocol { category: ReportType, location: LocationEntity?, photos: [Data] - ) async throws { + ) async throws -> Int? { let fileNames = (1...photos.count).map { "\($0).jpg" } // TODO: - 사진 업로드 실패 시 에러 처리 필요 guard let presignedDict = try await fileRepository.fetchPresignedURL(prefix: "report", fileNames: fileNames), presignedDict.count == photos.count - else { return } + else { return nil } let presignedURLs = Array(presignedDict.values) @@ -67,7 +67,7 @@ public final class ReportUseCase: ReportUseCaseProtocol { .first ?? url } - try await reportRepository.report( + return try await reportRepository.report( title: title, content: content, category: category, diff --git a/Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift b/Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift index 86236d43..60ed739b 100644 --- a/Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift +++ b/Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift @@ -80,9 +80,10 @@ final class ReportLoadingViewController: UIViewController { } extension ReportLoadingViewController: ReportRegistrationViewControllerDelegate { - func reportRegistrationViewController(_ sender: ReportRegistrationViewController, completeRegistration: Bool) { - if completeRegistration { + func reportRegistrationViewController(_ sender: ReportRegistrationViewController, completeRegistration reportId: Int?) { + if let reportId { let reportCompleteViewController = ReportCompleteViewController() + // TODO: - reportCompleteViewController에 제보id 전달 (또는 생성한 제보 객체 자체를 넘기기. 논의 필요) self.navigationController?.pushViewController(reportCompleteViewController, animated: true) } else { navigationController?.popViewController(animated: true) diff --git a/Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift b/Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift index 40615177..33f00b5d 100644 --- a/Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift +++ b/Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift @@ -12,7 +12,7 @@ import SnapKit import UIKit protocol ReportRegistrationViewControllerDelegate: AnyObject { - func reportRegistrationViewController(_ sender: ReportRegistrationViewController, completeRegistration: Bool) + func reportRegistrationViewController(_ sender: ReportRegistrationViewController, completeRegistration reportId: Int?) } final class ReportRegistrationViewController: BaseViewController { @@ -417,13 +417,10 @@ final class ReportRegistrationViewController: BaseViewController let isReportValid: AnyPublisher let exceptionPublisher: AnyPublisher - let reportRegistrationCompletePublisher: AnyPublisher + let reportRegistrationCompletePublisher: AnyPublisher let maxPhotoCount: Int } @@ -40,7 +40,7 @@ final class ReportRegistrationViewModel: ViewModel { private let locationSubject = CurrentValueSubject(nil) private let selectedPhotoSubject = CurrentValueSubject<[PhotoItem], Never>([]) private let reportVerificationSubject = PassthroughSubject() - private let reportRegistrationCompleteSubject = PassthroughSubject() + private let reportRegistrationCompleteSubject = PassthroughSubject() private let exceptionSubject = PassthroughSubject() private let maxPhotoCount = 3 private var location: LocationEntity? = nil @@ -146,19 +146,17 @@ final class ReportRegistrationViewModel: ViewModel { Task { let minimumDuration: TimeInterval = 0.7 let startTime = Date() - let result: Bool + let reportId: Int? do { - try await reportUseCase.report( + reportId = try await reportUseCase.report( title: name, content: content, category: category, location: location, photos: selectedPhotos) - result = true } catch { - - result = false + reportId = nil } let elapsed = Date().timeIntervalSince(startTime) @@ -169,7 +167,7 @@ final class ReportRegistrationViewModel: ViewModel { ) } - reportRegistrationCompleteSubject.send(result) + reportRegistrationCompleteSubject.send(reportId) } } From 3d08636565af734ba8bc28d77bda449cd1f68a0e Mon Sep 17 00:00:00 2001 From: taipaise Date: Sat, 22 Nov 2025 20:39:35 +0900 Subject: [PATCH 7/7] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...dConditaionDTO.swift => FilePresignedConditionDTO.swift} | 4 ++-- .../DataSource/Sources/Endpoint/FilePresignedEndpoint.swift | 2 +- Projects/DataSource/Sources/Repository/FileRepository.swift | 2 +- ...epositoryProtocol.swift => FileRepositoryProtocol.swift} | 2 +- Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift | 6 +++++- 5 files changed, 10 insertions(+), 6 deletions(-) rename Projects/DataSource/Sources/DTO/{FilePresignedConditaionDTO.swift => FilePresignedConditionDTO.swift} (58%) rename Projects/Domain/Sources/Protocol/Repository/{FIileRepositoryProtocol.swift => FileRepositoryProtocol.swift} (95%) diff --git a/Projects/DataSource/Sources/DTO/FilePresignedConditaionDTO.swift b/Projects/DataSource/Sources/DTO/FilePresignedConditionDTO.swift similarity index 58% rename from Projects/DataSource/Sources/DTO/FilePresignedConditaionDTO.swift rename to Projects/DataSource/Sources/DTO/FilePresignedConditionDTO.swift index 451561fb..7f141531 100644 --- a/Projects/DataSource/Sources/DTO/FilePresignedConditaionDTO.swift +++ b/Projects/DataSource/Sources/DTO/FilePresignedConditionDTO.swift @@ -1,11 +1,11 @@ // -// FilePresignedDTO.swift +// FilePresignedConditionDTO.swift // DataSource // // Created by 이동현 on 11/22/25. // -struct FilePresignedConditaionDTO: Codable { +struct FilePresignedConditionDTO: Codable { let prefix: String? let fileName: String } diff --git a/Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift b/Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift index 91548cb1..b6be9683 100644 --- a/Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift +++ b/Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift @@ -8,7 +8,7 @@ import Foundation enum FilePresignedEndpoint { - case fetchPresignedURL(presignedConditions: [FilePresignedConditaionDTO]) + case fetchPresignedURL(presignedConditions: [FilePresignedConditionDTO]) } extension FilePresignedEndpoint: Endpoint { diff --git a/Projects/DataSource/Sources/Repository/FileRepository.swift b/Projects/DataSource/Sources/Repository/FileRepository.swift index 978b3940..7d7aa3c4 100644 --- a/Projects/DataSource/Sources/Repository/FileRepository.swift +++ b/Projects/DataSource/Sources/Repository/FileRepository.swift @@ -12,7 +12,7 @@ final class FileRepository: FileRepositoryProtocol { private let networkService = NetworkService.shared func fetchPresignedURL(prefix: String?, fileNames: [String]) async throws -> [String : String]? { - let dtos = fileNames.map { FilePresignedConditaionDTO(prefix: prefix, fileName: $0) } + let dtos = fileNames.map { FilePresignedConditionDTO(prefix: prefix, fileName: $0) } let endpoint = FilePresignedEndpoint.fetchPresignedURL(presignedConditions: dtos) return try await networkService.request(endpoint: endpoint, type: [String:String].self) diff --git a/Projects/Domain/Sources/Protocol/Repository/FIileRepositoryProtocol.swift b/Projects/Domain/Sources/Protocol/Repository/FileRepositoryProtocol.swift similarity index 95% rename from Projects/Domain/Sources/Protocol/Repository/FIileRepositoryProtocol.swift rename to Projects/Domain/Sources/Protocol/Repository/FileRepositoryProtocol.swift index 8f6eee1f..67147bcf 100644 --- a/Projects/Domain/Sources/Protocol/Repository/FIileRepositoryProtocol.swift +++ b/Projects/Domain/Sources/Protocol/Repository/FileRepositoryProtocol.swift @@ -1,5 +1,5 @@ // -// FIileRepository.swift +// FileRepositoryProtocol.swift // Domain // // Created by 이동현 on 11/22/25. diff --git a/Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift b/Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift index b57bcb9b..fa54f155 100644 --- a/Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift +++ b/Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift @@ -43,6 +43,8 @@ public final class ReportUseCase: ReportUseCaseProtocol { location: LocationEntity?, photos: [Data] ) async throws -> Int? { + if photos.isEmpty { return nil } + let fileNames = (1...photos.count).map { "\($0).jpg" } // TODO: - 사진 업로드 실패 시 에러 처리 필요 @@ -51,7 +53,9 @@ public final class ReportUseCase: ReportUseCaseProtocol { presignedDict.count == photos.count else { return nil } - let presignedURLs = Array(presignedDict.values) + let presignedURLs = fileNames.compactMap { fileName in + presignedDict[fileName] + } for (url, photo) in zip(presignedURLs, photos) { do {