diff --git a/APIService/Package.swift b/APIService/Package.swift index eb03b55..8aa794d 100644 --- a/APIService/Package.swift +++ b/APIService/Package.swift @@ -6,6 +6,14 @@ let package = Package( name: "APIService", platforms: [.iOS(.v17)], products: [ + .library( + name: "CreditsAPI", + targets: ["CreditsAPI"] + ), + .library( + name: "CreditsAPISupport", + targets: ["CreditsAPISupport"] + ), .library( name: "HomeAPI", targets: ["HomeAPI"] @@ -44,6 +52,20 @@ let package = Package( .package(path: "../Entity"), ], targets: [ + .target( + name: "CreditsAPI", + dependencies: [ + .product(name: "Network", package: "Core"), + .product(name: "Entity", package: "Entity"), + ] + ), + .target( + name: "CreditsAPISupport", + dependencies: [ + "CreditsAPI", + ], + resources: [.process("Mocks")] + ), .target( name: "HomeAPI", dependencies: [ diff --git a/APIService/Sources/CreditsAPI/CreditsEndPoint.swift b/APIService/Sources/CreditsAPI/CreditsEndPoint.swift new file mode 100644 index 0000000..6facf6b --- /dev/null +++ b/APIService/Sources/CreditsAPI/CreditsEndPoint.swift @@ -0,0 +1,35 @@ +// +// CreditsEndPoint.swift +// +// +// Created by 홍승현 on 1/25/24. +// + +import Foundation +import Network + +// MARK: - CreditsEndPoint + +public enum CreditsEndPoint { + case fetchCredits +} + +// MARK: EndPoint + +extension CreditsEndPoint: EndPoint { + public var method: HTTPMethod { + .get + } + + public var path: String { + "/v2/credits" + } + + public var parameters: HTTPParameter { + .plain + } + + public var headers: [String: String] { + ["Content-Type": "application/json"] + } +} diff --git a/APIService/Sources/CreditsAPI/CreditsService.swift b/APIService/Sources/CreditsAPI/CreditsService.swift new file mode 100644 index 0000000..d6dca44 --- /dev/null +++ b/APIService/Sources/CreditsAPI/CreditsService.swift @@ -0,0 +1,45 @@ +// +// CreditsService.swift +// +// +// Created by 홍승현 on 1/31/24. +// + +import Entity +import Foundation +import Network + +// MARK: - CreditsServiceRepresentable + +public protocol CreditsServiceRepresentable { + func fetchCredits() async throws -> Credits +} + +// MARK: - CreditsService + +public struct CreditsService { + private let network: Networking + + public init(network: Networking) { + self.network = network + } +} + +// MARK: CreditsServiceRepresentable + +extension CreditsService: CreditsServiceRepresentable { + public func fetchCredits() async throws -> Credits { + let response: CreditsResponse = try await network.request(with: CreditsEndPoint.fetchCredits) + return Credits(dto: response) + } +} + +private extension Credits { + init(dto: CreditsResponse) { + self.init( + title: dto.title, + body: dto.body, + date: dto.date + ) + } +} diff --git a/APIService/Sources/CreditsAPI/Responses/CreditsResponse.swift b/APIService/Sources/CreditsAPI/Responses/CreditsResponse.swift new file mode 100644 index 0000000..941e67b --- /dev/null +++ b/APIService/Sources/CreditsAPI/Responses/CreditsResponse.swift @@ -0,0 +1,17 @@ +// +// CreditsResponse.swift +// +// +// Created by 홍승현 on 1/31/24. +// + +import Entity +import Foundation + +// MARK: - ProductResponse + +struct CreditsResponse: Decodable { + let title: String + let body: String + let date: Date +} diff --git a/APIService/Sources/CreditsAPISupport/CreditsURLProtocol.swift b/APIService/Sources/CreditsAPISupport/CreditsURLProtocol.swift new file mode 100644 index 0000000..70a1e3e --- /dev/null +++ b/APIService/Sources/CreditsAPISupport/CreditsURLProtocol.swift @@ -0,0 +1,47 @@ +// +// CreditsURLProtocol.swift +// +// +// Created by 홍승현 on 1/25/24. +// + +import CreditsAPI +import Foundation +import Network + +public final class CreditsURLProtocol: URLProtocol { + private lazy var mockData: [String: Data?] = [ + CreditsEndPoint.fetchCredits.path: loadMockData(fileName: "CreditsResponse"), + ] + + override public class func canInit(with _: URLRequest) -> Bool { + true + } + + override public class func canonicalRequest(for request: URLRequest) -> URLRequest { + request + } + + override public func startLoading() { + defer { client?.urlProtocolDidFinishLoading(self) } + if let url = request.url, + let mockData = mockData[url.path(percentEncoded: true)], + let data = mockData, + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) { + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + } else { + client?.urlProtocol(self, didFailWithError: NetworkError.urlError) + } + } + + override public func stopLoading() {} + + private func loadMockData(fileName: String) -> Data? { + guard let url = Bundle.module.url(forResource: fileName, withExtension: "json") + else { + return nil + } + return try? Data(contentsOf: url) + } +} diff --git a/APIService/Sources/CreditsAPISupport/Mocks/CreditsResponse.json b/APIService/Sources/CreditsAPISupport/Mocks/CreditsResponse.json new file mode 100644 index 0000000..99ac366 --- /dev/null +++ b/APIService/Sources/CreditsAPISupport/Mocks/CreditsResponse.json @@ -0,0 +1,5 @@ +{ + "title": "만든 사람들", + "body": "편행을 함께 만든 사람들을 소개합니다.\r\n\r\n기획/디자인\r\n- 박상훈, 홍승현\r\n\r\n개발\r\n- 홍승현과 아이들\r\n\r\n다국어 지원\r\n- 영어\r\n- 일본어", + "date": "2024-03-20T01:07:55Z" +} diff --git a/Entity/Sources/Entity/Credits/Credits.swift b/Entity/Sources/Entity/Credits/Credits.swift new file mode 100644 index 0000000..5ecc4c8 --- /dev/null +++ b/Entity/Sources/Entity/Credits/Credits.swift @@ -0,0 +1,26 @@ +// +// Credits.swift +// +// +// Created by 홍승현 on 3/22/24. +// + +import Foundation + +/// `만든사람들`을 보여주는 크레딧 장면에서 사용될 엔티티 모델 +public struct Credits { + /// 크레딧 제목 + public let title: String + + /// 크레딧 내용 + public let body: String + + /// 크레딧 생성 날짜 + public let date: Date + + public init(title: String, body: String, date: Date) { + self.title = title + self.body = body + self.date = date + } +} diff --git a/PyeonHaeng-iOS.xcodeproj/project.pbxproj b/PyeonHaeng-iOS.xcodeproj/project.pbxproj index a68cb7c..609a226 100644 --- a/PyeonHaeng-iOS.xcodeproj/project.pbxproj +++ b/PyeonHaeng-iOS.xcodeproj/project.pbxproj @@ -36,7 +36,6 @@ BA4EA3602B6A37E10003DCE7 /* Entity in Frameworks */ = {isa = PBXBuildFile; productRef = BA4EA35F2B6A37E10003DCE7 /* Entity */; }; BA4FB9C52BA1BC5B00ED03C4 /* NoticeAPI in Frameworks */ = {isa = PBXBuildFile; productRef = BA4FB9C42BA1BC5B00ED03C4 /* NoticeAPI */; }; BA4FB9C72BA1BC5B00ED03C4 /* NoticeAPISupport in Frameworks */ = {isa = PBXBuildFile; productRef = BA4FB9C62BA1BC5B00ED03C4 /* NoticeAPISupport */; }; - BA5E51932B9AC8510036209A /* NoticeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5E51922B9AC8510036209A /* NoticeDetailView.swift */; }; BA849A342B8F4F36004495BF /* ProductConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8E83232B8EF83B00FE968C /* ProductConfiguration.swift */; }; BA849A372B8F4FC0004495BF /* MockPaginatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA849A362B8F4FC0004495BF /* MockPaginatable.swift */; }; BA8E83242B8EF83B00FE968C /* ProductConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8E83232B8EF83B00FE968C /* ProductConfiguration.swift */; }; @@ -52,6 +51,9 @@ BAE159D82B65FA6F002DCF94 /* HomeProductDetailSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE159D72B65FA6F002DCF94 /* HomeProductDetailSelectionView.swift */; }; BAE159DA2B65FC35002DCF94 /* HomeProductListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE159D92B65FC35002DCF94 /* HomeProductListView.swift */; }; BAE159DE2B663A9A002DCF94 /* HomeProductSorterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE159DD2B663A9A002DCF94 /* HomeProductSorterView.swift */; }; + BAE5ADB02BAD61D000176ADE /* CreditsAPI in Frameworks */ = {isa = PBXBuildFile; productRef = BAE5ADAF2BAD61D000176ADE /* CreditsAPI */; }; + BAE5ADB22BAD61D000176ADE /* CreditsAPISupport in Frameworks */ = {isa = PBXBuildFile; productRef = BAE5ADB12BAD61D000176ADE /* CreditsAPISupport */; }; + BAE5ADB92BAD660500176ADE /* CreditsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE5ADB82BAD660500176ADE /* CreditsView.swift */; }; BAE7A1E62B9D7CA70022FEBB /* MailRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE7A1E52B9D7CA70022FEBB /* MailRowItem.swift */; }; BAE7A1E82B9DA0360022FEBB /* DeviceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE7A1E72B9DA0360022FEBB /* DeviceProvider.swift */; }; BAE7A1EA2B9DA4960022FEBB /* MockDeviceInformationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE7A1E92B9DA4960022FEBB /* MockDeviceInformationProvider.swift */; }; @@ -113,7 +115,6 @@ BA28F1A72B6157E90052855E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BA402F7D2B85E31800E86AAD /* ConvenienceSelectBottomSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvenienceSelectBottomSheetView.swift; sourceTree = ""; }; BA4EA35A2B6A00F70003DCE7 /* Entity */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Entity; sourceTree = ""; }; - BA5E51922B9AC8510036209A /* NoticeDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeDetailView.swift; sourceTree = ""; }; BA849A362B8F4FC0004495BF /* MockPaginatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPaginatable.swift; sourceTree = ""; }; BA8E83232B8EF83B00FE968C /* ProductConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductConfiguration.swift; sourceTree = ""; }; BAA4D9A92B5A1795005999F8 /* PyeonHaeng-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PyeonHaeng-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -129,6 +130,7 @@ BAE159D72B65FA6F002DCF94 /* HomeProductDetailSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeProductDetailSelectionView.swift; sourceTree = ""; }; BAE159D92B65FC35002DCF94 /* HomeProductListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeProductListView.swift; sourceTree = ""; }; BAE159DD2B663A9A002DCF94 /* HomeProductSorterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeProductSorterView.swift; sourceTree = ""; }; + BAE5ADB82BAD660500176ADE /* CreditsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsView.swift; sourceTree = ""; }; BAE7A1E52B9D7CA70022FEBB /* MailRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailRowItem.swift; sourceTree = ""; }; BAE7A1E72B9DA0360022FEBB /* DeviceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProvider.swift; sourceTree = ""; }; BAE7A1E92B9DA4960022FEBB /* MockDeviceInformationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDeviceInformationProvider.swift; sourceTree = ""; }; @@ -156,6 +158,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + BAE5ADB22BAD61D000176ADE /* CreditsAPISupport in Frameworks */, BA05640D2B6248EA003D6DC7 /* Network in Frameworks */, BA4FB9C72BA1BC5B00ED03C4 /* NoticeAPISupport in Frameworks */, E57F2AA62B7717EA00E12B3D /* ProductInfoAPISupport in Frameworks */, @@ -165,6 +168,7 @@ BA05640B2B6248EA003D6DC7 /* HomeAPISupport in Frameworks */, BA0623D82B68E51400A0A3B2 /* Log in Frameworks */, BA0564092B6248EA003D6DC7 /* HomeAPI in Frameworks */, + BAE5ADB02BAD61D000176ADE /* CreditsAPI in Frameworks */, BAB569612B639F3000D1E0F9 /* DesignSystem in Frameworks */, E55DD5182B9370D100AA63C0 /* SearchAPI in Frameworks */, E55DD51A2B9370D100AA63C0 /* SearchAPISupport in Frameworks */, @@ -215,7 +219,6 @@ isa = PBXGroup; children = ( BA1688DF2B99B85500A8F462 /* NoticeView.swift */, - BA5E51922B9AC8510036209A /* NoticeDetailView.swift */, ); path = View; sourceTree = ""; @@ -309,6 +312,7 @@ BA28F18C2B6155EC0052855E /* SettingsScene */ = { isa = PBXGroup; children = ( + BAE5ADB32BAD638200176ADE /* Credits */, BAB8C3AB2BAC7A09003DF3CC /* LeaveReview */, BA097F0C2B9CA820002D3E1E /* Mail */, BA1688EB2B99D0B700A8F462 /* Notice */, @@ -438,6 +442,14 @@ path = LeaveReview; sourceTree = ""; }; + BAE5ADB32BAD638200176ADE /* Credits */ = { + isa = PBXGroup; + children = ( + BAE5ADB82BAD660500176ADE /* CreditsView.swift */, + ); + path = Credits; + sourceTree = ""; + }; E5462C6B2B65CF7800E9FDF2 /* Components */ = { isa = PBXGroup; children = ( @@ -495,6 +507,8 @@ E55DD5192B9370D100AA63C0 /* SearchAPISupport */, BA4FB9C42BA1BC5B00ED03C4 /* NoticeAPI */, BA4FB9C62BA1BC5B00ED03C4 /* NoticeAPISupport */, + BAE5ADAF2BAD61D000176ADE /* CreditsAPI */, + BAE5ADB12BAD61D000176ADE /* CreditsAPISupport */, ); productName = "PyeonHaeng-iOS"; productReference = BAA4D9A92B5A1795005999F8 /* PyeonHaeng-iOS.app */; @@ -635,7 +649,6 @@ BAE7A1E62B9D7CA70022FEBB /* MailRowItem.swift in Sources */, E5028D5C2B96BA9400B36C16 /* SearchView.swift in Sources */, BAE159DA2B65FC35002DCF94 /* HomeProductListView.swift in Sources */, - BA5E51932B9AC8510036209A /* NoticeDetailView.swift in Sources */, BA402F7E2B85E31800E86AAD /* ConvenienceSelectBottomSheetView.swift in Sources */, BA19897C2BA95E8C0083B4BE /* DIContainer.swift in Sources */, BA28F1852B6155810052855E /* OnboardingView.swift in Sources */, @@ -643,6 +656,7 @@ BA28F1882B6155910052855E /* HomeView.swift in Sources */, E5F2EC452B64926100EE0838 /* PromotionTagView.swift in Sources */, BA28F18B2B6155BD0052855E /* ProductInfoView.swift in Sources */, + BAE5ADB92BAD660500176ADE /* CreditsView.swift in Sources */, E5462C662B65677B00E9FDF2 /* PromotionTag.swift in Sources */, E50584532B763C8C002FDACF /* ProductInfoViewModel.swift in Sources */, BAB720342B9325F200C2CA1A /* PromotionSelectBottomSheetView.swift in Sources */, @@ -1090,6 +1104,14 @@ isa = XCSwiftPackageProductDependency; productName = DesignSystem; }; + BAE5ADAF2BAD61D000176ADE /* CreditsAPI */ = { + isa = XCSwiftPackageProductDependency; + productName = CreditsAPI; + }; + BAE5ADB12BAD61D000176ADE /* CreditsAPISupport */ = { + isa = XCSwiftPackageProductDependency; + productName = CreditsAPISupport; + }; E55DD5172B9370D100AA63C0 /* SearchAPI */ = { isa = XCSwiftPackageProductDependency; productName = SearchAPI; diff --git a/PyeonHaeng-iOS/Sources/Scenes/SettingsScene/Credits/CreditsView.swift b/PyeonHaeng-iOS/Sources/Scenes/SettingsScene/Credits/CreditsView.swift new file mode 100644 index 0000000..bb127fd --- /dev/null +++ b/PyeonHaeng-iOS/Sources/Scenes/SettingsScene/Credits/CreditsView.swift @@ -0,0 +1,29 @@ +// +// CreditsView.swift +// PyeonHaeng-iOS +// +// Created by 홍승현 on 3/22/24. +// + +import DesignSystem +import SwiftUI + +struct CreditsView: View { + @Environment(\.injected) private var container + @State private var content: String = "" + + var body: some View { + MarkdownView(content: content) + .navigationTitle("Credits") + .onAppear { + Task { + let credits = try await container.services.creditsService.fetchCredits() + content = credits.body + } + } + } +} + +#Preview { + SettingsView() +} diff --git a/PyeonHaeng-iOS/Sources/Scenes/SettingsScene/Notice/View/NoticeView.swift b/PyeonHaeng-iOS/Sources/Scenes/SettingsScene/Notice/View/NoticeView.swift index 1230d44..bf6eb7d 100644 --- a/PyeonHaeng-iOS/Sources/Scenes/SettingsScene/Notice/View/NoticeView.swift +++ b/PyeonHaeng-iOS/Sources/Scenes/SettingsScene/Notice/View/NoticeView.swift @@ -25,8 +25,34 @@ struct NoticeView: View where ViewModel: NoticeViewModelRepresentable List(viewModel.state.noticeList) { notice in ZStack(alignment: .leading) { NavigationLink { - NoticeDetailView() - .toolbarRole(.editor) + // TODO: API 연결 시 삭제할 예정입니다. + MarkdownView( + content: """ + 안녕하세요 편행팀입니다~ + 새로워진 2024년을 맞아 편행이 새단장으로 여러분을 찾아왔습니다. 앞으로도 우리 자주 만나요! + + [업데이트 정보] + - 새로운 버전(v.2.0.0)으로 리뉴얼 되었습니다. + - 새로워진 편행 2.0에서 더욱 더 합리적인 소비 생활 되세요! + - 앱 디자인 및 전반적인 UX를 개선해서 사용하기 편하게 했어요! + - 합리적인 소비를 도와드리고자 행사 상품 가격의 변동 정보를 그래프로 보여드려요! + + - 기타 자잘한 버그들을 수정했습니다. + - 단말 안정화 적용 + - 화면 및 네트워크 최적화 + + - 2023년 3월 편의점 행사정보가 업데이트 되었습니다. + + [새로운 기능] + - 지도 기능이 추가되었습니다. + - 카테고리가 추가되었습니다. + + [주의사항] + - 본 어플리케이션에서 표시된 정보는 실제 행사정보와 차이가 있을수 있습니다. + - 행사는 편의점 브랜드 및 재고상태에 따라 다르게 적용될 수 있습니다. + """ + ) + .toolbarRole(.editor) } label: { EmptyView() } diff --git a/PyeonHaeng-iOS/Sources/Scenes/SettingsScene/SettingsView.swift b/PyeonHaeng-iOS/Sources/Scenes/SettingsScene/SettingsView.swift index 93102c2..7f2d17a 100644 --- a/PyeonHaeng-iOS/Sources/Scenes/SettingsScene/SettingsView.swift +++ b/PyeonHaeng-iOS/Sources/Scenes/SettingsScene/SettingsView.swift @@ -79,8 +79,11 @@ private struct SettingsRow: View { case .leaveReview: LeaveReviewView() - default: - NavigationLink {} label: { + case .credits: + NavigationLink { + CreditsView() + .toolbarRole(.editor) + } label: { subject } } diff --git a/PyeonHaeng-iOS/Sources/Services.swift b/PyeonHaeng-iOS/Sources/Services.swift index 0a1be46..d6bb070 100644 --- a/PyeonHaeng-iOS/Sources/Services.swift +++ b/PyeonHaeng-iOS/Sources/Services.swift @@ -5,6 +5,8 @@ // Created by 홍승현 on 2/1/24. // +import CreditsAPI +import CreditsAPISupport import Foundation import HomeAPI import HomeAPISupport @@ -23,6 +25,8 @@ struct Services { let noticeService: NoticeServiceRepresentable let productInfoService: ProductInfoServiceRepresentable let searchService: SearchServiceRepresentable + let creditsService: CreditsServiceRepresentable + init() { let homeNetworking: Networking = { let configuration: URLSessionConfiguration @@ -35,6 +39,7 @@ struct Services { let provider = NetworkProvider(session: URLSession(configuration: configuration)) return provider }() + let noticeNetworking: Networking = { let configuration: URLSessionConfiguration #if DEBUG @@ -46,6 +51,7 @@ struct Services { let provider = NetworkProvider(session: URLSession(configuration: configuration)) return provider }() + let productInfoNetworking: Networking = { let configuration: URLSessionConfiguration #if DEBUG @@ -57,6 +63,7 @@ struct Services { let provider = NetworkProvider(session: URLSession(configuration: configuration)) return provider }() + let searchNetworking: Networking = { let configuration: URLSessionConfiguration #if DEBUG @@ -68,9 +75,23 @@ struct Services { let provider = NetworkProvider(session: URLSession(configuration: configuration)) return provider }() + + let creditsNetworking: Networking = { + let configuration: URLSessionConfiguration + #if DEBUG + configuration = .ephemeral + configuration.protocolClasses = [CreditsURLProtocol.self] + #else + configuration = .default + #endif + let provider = NetworkProvider(session: URLSession(configuration: configuration)) + return provider + }() + homeService = HomeService(network: homeNetworking) noticeService = NoticeService(network: noticeNetworking) productInfoService = ProductInfoService(network: productInfoNetworking) searchService = SearchService(network: searchNetworking) + creditsService = CreditsService(network: creditsNetworking) } } diff --git a/PyeonHaeng-iOS/Sources/Scenes/SettingsScene/Notice/View/NoticeDetailView.swift b/Shared/Sources/DesignSystem/Components/Images/MarkdownView.swift similarity index 60% rename from PyeonHaeng-iOS/Sources/Scenes/SettingsScene/Notice/View/NoticeDetailView.swift rename to Shared/Sources/DesignSystem/Components/Images/MarkdownView.swift index 8590c1e..830c32a 100644 --- a/PyeonHaeng-iOS/Sources/Scenes/SettingsScene/Notice/View/NoticeDetailView.swift +++ b/Shared/Sources/DesignSystem/Components/Images/MarkdownView.swift @@ -1,11 +1,10 @@ // -// NoticeDetailView.swift +// MarkdownView.swift // PyeonHaeng-iOS // // Created by 홍승현 on 3/8/24. // -import DesignSystem import SwiftUI // MARK: - TextBlock @@ -15,40 +14,20 @@ private struct TextBlock: Identifiable { let content: String } -// MARK: - NoticeDetailView +// MARK: - MarkdownView -struct NoticeDetailView: View { - // TODO: API 연결 시 지울 예정입니다. - private let noticeContent: String = """ - 안녕하세요 편행팀입니다~ - 새로워진 2024년을 맞아 편행이 새단장으로 여러분을 찾아왔습니다. 앞으로도 우리 자주 만나요! - - [업데이트 정보] - - 새로운 버전(v.2.0.0)으로 리뉴얼 되었습니다. - - 새로워진 편행 2.0에서 더욱 더 합리적인 소비 생활 되세요! - - 앱 디자인 및 전반적인 UX를 개선해서 사용하기 편하게 했어요! - - 합리적인 소비를 도와드리고자 행사 상품 가격의 변동 정보를 그래프로 보여드려요! - - - 기타 자잘한 버그들을 수정했습니다. - - 단말 안정화 적용 - - 화면 및 네트워크 최적화 - - - 2023년 3월 편의점 행사정보가 업데이트 되었습니다. - - [새로운 기능] - - 지도 기능이 추가되었습니다. - - 카테고리가 추가되었습니다. +public struct MarkdownView: View { + private let content: String - [주의사항] - - 본 어플리케이션에서 표시된 정보는 실제 행사정보와 차이가 있을수 있습니다. - - 행사는 편의점 브랜드 및 재고상태에 따라 다르게 적용될 수 있습니다. - """ + public init(content: String) { + self.content = content + } - var body: some View { + public var body: some View { ScrollView { VStack(alignment: .leading, spacing: .zero) { - ForEach(noticeContent.components(separatedBy: .newlines).map(TextBlock.init)) { block in - if block.content.starts(with: /\s*[-*]/) { + ForEach(content.components(separatedBy: .newlines).map(TextBlock.init)) { block in + if block.content.starts(with: #/\s*[-*]/#) { BulletTextView(content: block.content) .font(.body3) } else { @@ -76,7 +55,7 @@ private struct BulletTextView: View { /// /// - Parameter content: The bullet-prefixed content, optionally with leading spaces. init(content: String) { - guard let match = content.firstMatch(of: /(?\s*)[-*]\s*?(?[^\s].*)/) + guard let match = content.firstMatch(of: #/(?\s*)[-*]\s*?(?[^\s].*)/#) else { self.content = content spacingCount = 0 @@ -129,9 +108,3 @@ private enum Metrics { private enum Constants { static let bulletImageName: String = "circle.fill" } - -#if DEBUG - #Preview { - NoticeDetailView() - } -#endif