diff --git a/HotSpot/App/Project.swift b/HotSpot/App/Project.swift new file mode 100644 index 0000000..899be7b --- /dev/null +++ b/HotSpot/App/Project.swift @@ -0,0 +1,78 @@ +import ProjectDescription + +let project = Project( + name: "HotSpot", + organizationName: "Coby", + settings: .settings( + base: [ + "BASE_URL": SettingValue(stringLiteral: "http://webservice.recruit.co.jp/hotpepper"), + "API_KEY": SettingValue(stringLiteral: "8011379945b3b751"), + "SWIFT_VERSION": SettingValue(stringLiteral: "5.9"), + "DEVELOPMENT_TEAM": SettingValue(stringLiteral: "3Y8YH8GWMM") + ], + configurations: [ + .debug(name: .debug), + .release(name: .release) + ] + ), + targets: [ + .target( + name: "HotSpot", + destinations: [.iPhone], + product: .app, + bundleId: "com.coby.HotSpot", + deploymentTargets: .iOS("15.0"), + infoPlist: .extendingDefault( + with: [ + "CFBundleShortVersionString": .string("1.0.0"), + "CFBundleVersion": .string("0"), + "CFBundleDisplayName": .string("HotSpot"), + "BASE_URL": .string("http://webservice.recruit.co.jp/hotpepper"), + "API_KEY": .string("8011379945b3b751"), + "UILaunchScreen": .dictionary([ + "UIColorName": .string(""), + "UIImageName": .string("") + ]), + "NSAppTransportSecurity": .dictionary([ + "NSExceptionDomains": .dictionary([ + "webservice.recruit.co.jp": .dictionary([ + "NSExceptionAllowsInsecureHTTPLoads": .boolean(true) + ]) + ]) + ]), + "NSLocationWhenInUseUsageDescription": .string("周辺の店舗を表示するために位置情報が必要です。"), + "NSLocationAlwaysAndWhenInUseUsageDescription": .string("周辺の店舗を表示するために位置情報が必要です。") + ] + ), + sources: ["Sources/**"], + resources: ["Resources/**"], + dependencies: [ + .project(target: "Presentation", path: "../Presentation"), + .project(target: "Data", path: "../Data"), + .project(target: "Domain", path: "../Domain"), + .project(target: "Shared", path: "../Shared") + ] + ), + .target( + name: "HotSpotTests", + destinations: .iOS, + product: .unitTests, + bundleId: "com.coby.HotSpotTests", + infoPlist: .default, + sources: ["Tests/**"], + dependencies: [.target(name: "HotSpot")] + ) + ], + schemes: [ + .scheme( + name: "HotSpot Debug", + buildAction: .buildAction(targets: ["HotSpot"]), + runAction: .runAction(configuration: .debug) + ), + .scheme( + name: "HotSpot Release", + buildAction: .buildAction(targets: ["HotSpot"]), + runAction: .runAction(configuration: .release) + ) + ] +) \ No newline at end of file diff --git a/HotSpot/App/README.md b/HotSpot/App/README.md new file mode 100644 index 0000000..2cb998d --- /dev/null +++ b/HotSpot/App/README.md @@ -0,0 +1,22 @@ +# App モジュール + +## 概要 +Appモジュールは、アプリケーションのエントリーポイントと全体設定を担当します。 + +## 構造 +``` +App/ +├── Sources/ # アプリケーション設定 +├── Resources/ # リソースファイル +└── Tests/ # ユニットテスト +``` + +## 依存関係 +- Presentationモジュール +- Dataモジュール +- Domainモジュール +- Coreモジュール + +## 使用方法 +Appモジュールは、アプリケーションの起動と初期化を担当します。 +他のすべてのモジュールを統合し、アプリケーション全体の設定を行います。 \ No newline at end of file diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/1024.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..58c7d4b Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/120 1.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/120 1.png new file mode 100644 index 0000000..aa3dbf5 Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/120 1.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/120.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000..aa3dbf5 Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/152.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000..06b75dc Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/167.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000..53fdbc6 Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/180.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000..e1a4894 Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/20.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000..ec18a8a Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/29.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000..ca12c94 Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/40 1.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/40 1.png new file mode 100644 index 0000000..7856fd1 Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/40 1.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/40 2.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/40 2.png new file mode 100644 index 0000000..7856fd1 Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/40 2.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/40.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..7856fd1 Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/58 1.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/58 1.png new file mode 100644 index 0000000..d920adc Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/58 1.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/58.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000..d920adc Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/60.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000..ad66aa5 Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/76.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000..1f132b2 Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/80 1.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/80 1.png new file mode 100644 index 0000000..f889408 Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/80 1.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/80.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000..f889408 Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/87.png b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000..d81ad3d Binary files /dev/null and b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13b0126 --- /dev/null +++ b/HotSpot/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "filename" : "40.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "58.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "80.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120 1.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "40 1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58 1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "40 2.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "80 1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "152.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "167.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HotSpot/Sources/App/AppDelegate.swift b/HotSpot/App/Sources/AppDelegate.swift similarity index 100% rename from HotSpot/Sources/App/AppDelegate.swift rename to HotSpot/App/Sources/AppDelegate.swift diff --git a/HotSpot/Sources/App/HotSpotApp.swift b/HotSpot/App/Sources/HotSpotApp.swift similarity index 100% rename from HotSpot/Sources/App/HotSpotApp.swift rename to HotSpot/App/Sources/HotSpotApp.swift diff --git a/HotSpot/Sources/App/SceneDelegate.swift b/HotSpot/App/Sources/SceneDelegate.swift similarity index 96% rename from HotSpot/Sources/App/SceneDelegate.swift rename to HotSpot/App/Sources/SceneDelegate.swift index 8e330c0..0ae87da 100644 --- a/HotSpot/Sources/App/SceneDelegate.swift +++ b/HotSpot/App/Sources/SceneDelegate.swift @@ -1,5 +1,6 @@ import SwiftUI import UIKit +import Presentation class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? @@ -16,4 +17,4 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { coordinator = AppCoordinator(window: window) coordinator?.start() } -} \ No newline at end of file +} diff --git a/HotSpot/App/Tests/HotSpotTests.swift b/HotSpot/App/Tests/HotSpotTests.swift new file mode 100644 index 0000000..eea734a --- /dev/null +++ b/HotSpot/App/Tests/HotSpotTests.swift @@ -0,0 +1,10 @@ +import XCTest +@testable import HotSpot + +final class HotSpotTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + XCTAssertTrue(true) + } +} diff --git a/HotSpot/Data/Project.swift b/HotSpot/Data/Project.swift new file mode 100644 index 0000000..6e4c64f --- /dev/null +++ b/HotSpot/Data/Project.swift @@ -0,0 +1,30 @@ +import ProjectDescription + +let project = Project( + name: "Data", + organizationName: "Coby", + targets: [ + .target( + name: "Data", + destinations: .iOS, + product: .framework, + bundleId: "com.coby.HotSpot.Data", + deploymentTargets: .iOS("15.0"), + infoPlist: .default, + sources: ["Sources/**"], + dependencies: [ + .external(name: "Moya"), + .project(target: "Shared", path: "../Shared") + ] + ), + .target( + name: "DataTests", + destinations: .iOS, + product: .unitTests, + bundleId: "com.coby.HotSpot.DataTests", + infoPlist: .default, + sources: ["Tests/**"], + dependencies: [.target(name: "Data")] + ) + ] +) \ No newline at end of file diff --git a/HotSpot/Data/README.md b/HotSpot/Data/README.md new file mode 100644 index 0000000..29b9dee --- /dev/null +++ b/HotSpot/Data/README.md @@ -0,0 +1,22 @@ +# Data モジュール + +## 概要 +Dataモジュールは、データの取得と永続化を担当します。 + +## 構造 +``` +Data/ +├── Sources/ +│ ├── Repository/ # リポジトリ実装 +│ ├── Network/ # ネットワーク関連 +│ └── Local/ # ローカルストレージ +└── Tests/ # ユニットテスト +``` + +## 依存関係 +- Domainモジュール +- Moya + +## 使用方法 +Dataモジュールは、外部データソース(API、ローカルストレージ)との通信を担当します。 +Domainモジュールのリポジトリインターフェースを実装します。 \ No newline at end of file diff --git a/HotSpot/Sources/Data/API/ServiceAPI.swift b/HotSpot/Data/Sources/API/ServiceAPI.swift similarity index 81% rename from HotSpot/Sources/Data/API/ServiceAPI.swift rename to HotSpot/Data/Sources/API/ServiceAPI.swift index c5d776a..c3d9d54 100644 --- a/HotSpot/Sources/Data/API/ServiceAPI.swift +++ b/HotSpot/Data/Sources/API/ServiceAPI.swift @@ -1,12 +1,12 @@ import Foundation import Moya -enum ServiceAPI { +public enum ServiceAPI { case searchShops(ShopSearchRequestDTO) } extension ServiceAPI: TargetType { - var baseURL: URL { + public var baseURL: URL { guard let baseURLString = Bundle.main.object(forInfoDictionaryKey: "BASE_URL") as? String, let url = URL(string: baseURLString) else { fatalError("BASE_URL is not set in configuration") @@ -14,21 +14,21 @@ extension ServiceAPI: TargetType { return url } - var path: String { + public var path: String { switch self { case .searchShops: return "/gourmet/v1/" } } - var method: Moya.Method { + public var method: Moya.Method { switch self { case .searchShops: return .get } } - var task: Task { + public var task: Task { guard let apiKey = Bundle.main.object(forInfoDictionaryKey: "API_KEY") as? String else { fatalError("API_KEY is not set in configuration") } @@ -42,11 +42,11 @@ extension ServiceAPI: TargetType { } } - var headers: [String: String]? { + public var headers: [String: String]? { return nil } - var validationType: ValidationType { + public var validationType: ValidationType { return .successCodes } } diff --git a/HotSpot/Data/Sources/DTO/Request/ShopSearchRequestDTO.swift b/HotSpot/Data/Sources/DTO/Request/ShopSearchRequestDTO.swift new file mode 100644 index 0000000..7661a9b --- /dev/null +++ b/HotSpot/Data/Sources/DTO/Request/ShopSearchRequestDTO.swift @@ -0,0 +1,65 @@ +import Foundation + +public struct ShopSearchRequestDTO { + public let lat: Double // Latitude + public let lng: Double // Longitude + public let range: Int // Search range (1–5) + public let count: Int? // Number of results (1–100) + public let name: String? // Name search + public let genres: [String]? // Genre codes + public let start: Int? // Starting index for paging + public let budgets: [String]? // Budget codes + public let privateRoom: Int // Private room availability (0 or 1) + public let wifi: Int // Wi-Fi availability (0 or 1) + public let nonSmoking: Int // Non-smoking availability (0 or 1) + public let parking: Int // Parking availability (0 or 1) + + public init( + lat: Double, + lng: Double, + range: Int, + count: Int? = nil, + name: String? = nil, + genres: [String]? = nil, + start: Int? = nil, + budgets: [String]? = nil, + privateRoom: Int, + wifi: Int, + nonSmoking: Int, + parking: Int + ) { + self.lat = lat + self.lng = lng + self.range = range + self.count = count + self.name = name + self.genres = genres + self.start = start + self.budgets = budgets + self.privateRoom = privateRoom + self.wifi = wifi + self.nonSmoking = nonSmoking + self.parking = parking + } + + /// Converts the DTO into a dictionary of parameters for Moya or URL encoding + public var asParameters: [String: Any] { + var params: [String: Any] = [ + "lat": lat, + "lng": lng, + "range": range, + "private_room": privateRoom, + "wifi": wifi, + "non_smoking": nonSmoking, + "parking": parking + ] + + if let count = count { params["count"] = count } + if let name = name { params["name"] = name } + if let genres = genres, !genres.isEmpty { params["genre"] = genres.joined(separator: ",") } + if let start = start { params["start"] = start } + if let budgets = budgets, !budgets.isEmpty { params["budget"] = budgets.joined(separator: ",") } + + return params + } +} diff --git a/HotSpot/Data/Sources/DTO/Response/ShopDTO.swift b/HotSpot/Data/Sources/DTO/Response/ShopDTO.swift new file mode 100644 index 0000000..53e2b9f --- /dev/null +++ b/HotSpot/Data/Sources/DTO/Response/ShopDTO.swift @@ -0,0 +1,75 @@ +import Foundation + +public struct ShopDTO: Decodable { + public let id: String + public let name: String + public let address: String + public let lat: Double + public let lng: Double + public let access: String + public let open: String? + public let photo: Photo + public let genre: Genre + public let budget: Budget? + public let wifi: String? + public let nonSmoking: String? + public let privateRoom: String? + public let parking: String? + public let urls: Urls + + public struct Photo: Decodable { + public let pc: PcPhoto + + public struct PcPhoto: Decodable { + public let large: String + public let medium: String + public let small: String + + public enum CodingKeys: String, CodingKey { + case large = "l" + case medium = "m" + case small = "s" + } + } + } + + public struct Genre: Decodable { + public let code: String + public let name: String + public let catchPhrase: String + + public enum CodingKeys: String, CodingKey { + case code + case name + case catchPhrase = "catch" + } + } + + public struct Budget: Decodable { + public let code: String + public let name: String + public let average: String? + } + + public struct Urls: Decodable { + public let pc: String + } + + public enum CodingKeys: String, CodingKey { + case id + case name + case address + case lat + case lng + case access + case open + case photo + case genre + case budget + case wifi + case nonSmoking = "non_smoking" + case privateRoom = "private_room" + case parking + case urls + } +} diff --git a/HotSpot/Data/Sources/DTO/Response/ShopSearchResponseDTO.swift b/HotSpot/Data/Sources/DTO/Response/ShopSearchResponseDTO.swift new file mode 100644 index 0000000..d693d1d --- /dev/null +++ b/HotSpot/Data/Sources/DTO/Response/ShopSearchResponseDTO.swift @@ -0,0 +1,9 @@ +import Foundation + +public struct ShopSearchResponseDTO: Decodable { + public let results: ShopSearchResultsDTO + + public enum CodingKeys: String, CodingKey { + case results + } +} diff --git a/HotSpot/Data/Sources/DTO/Response/ShopSearchResultsDTO.swift b/HotSpot/Data/Sources/DTO/Response/ShopSearchResultsDTO.swift new file mode 100644 index 0000000..d692a2b --- /dev/null +++ b/HotSpot/Data/Sources/DTO/Response/ShopSearchResultsDTO.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct ShopSearchResultsDTO: Decodable { + public let resultsAvailable: Int + public let resultsReturned: String + public let resultsStart: Int + public let shop: [ShopDTO] + + public enum CodingKeys: String, CodingKey { + case resultsAvailable = "results_available" + case resultsReturned = "results_returned" + case resultsStart = "results_start" + case shop + } +} diff --git a/HotSpot/Sources/Data/DataSource/ShopRemoteDataSource.swift b/HotSpot/Data/Sources/DataSource/ShopRemoteDataSource.swift similarity index 73% rename from HotSpot/Sources/Data/DataSource/ShopRemoteDataSource.swift rename to HotSpot/Data/Sources/DataSource/ShopRemoteDataSource.swift index 5b8d0c2..ddd27fa 100644 --- a/HotSpot/Sources/Data/DataSource/ShopRemoteDataSource.swift +++ b/HotSpot/Data/Sources/DataSource/ShopRemoteDataSource.swift @@ -1,5 +1,5 @@ import Foundation -protocol ShopRemoteDataSource { +public protocol ShopRemoteDataSource { func search(request: ShopSearchRequestDTO) async throws -> ShopSearchResponseDTO } diff --git a/HotSpot/Sources/Data/DataSource/ShopRemoteDataSourceImpl.swift b/HotSpot/Data/Sources/DataSource/ShopRemoteDataSourceImpl.swift similarity index 58% rename from HotSpot/Sources/Data/DataSource/ShopRemoteDataSourceImpl.swift rename to HotSpot/Data/Sources/DataSource/ShopRemoteDataSourceImpl.swift index 04c0ae3..4f1e674 100644 --- a/HotSpot/Sources/Data/DataSource/ShopRemoteDataSourceImpl.swift +++ b/HotSpot/Data/Sources/DataSource/ShopRemoteDataSourceImpl.swift @@ -1,14 +1,15 @@ import Foundation import Moya +import Shared -final class ShopRemoteDataSourceImpl: ShopRemoteDataSource { +public final class ShopRemoteDataSourceImpl: ShopRemoteDataSource { private let provider: MoyaProvider - init(provider: MoyaProvider = .default) { + public init(provider: MoyaProvider = .default) { self.provider = provider } - func search(request: ShopSearchRequestDTO) async throws -> ShopSearchResponseDTO { + public func search(request: ShopSearchRequestDTO) async throws -> ShopSearchResponseDTO { let target = ServiceAPI.searchShops(request) let response = try await provider.asyncRequest(target) return try JSONDecoder().decode(ShopSearchResponseDTO.self, from: response.data) diff --git a/HotSpot/Data/Tests/DataTests.swift b/HotSpot/Data/Tests/DataTests.swift new file mode 100644 index 0000000..898f66a --- /dev/null +++ b/HotSpot/Data/Tests/DataTests.swift @@ -0,0 +1,10 @@ +import XCTest +@testable import Data + +final class DataTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + XCTAssertTrue(true) + } +} \ No newline at end of file diff --git a/HotSpot/Domain/Project.swift b/HotSpot/Domain/Project.swift new file mode 100644 index 0000000..bd19b14 --- /dev/null +++ b/HotSpot/Domain/Project.swift @@ -0,0 +1,30 @@ +import ProjectDescription + +let project = Project( + name: "Domain", + organizationName: "Coby", + targets: [ + .target( + name: "Domain", + destinations: [.iPhone], + product: .framework, + bundleId: "com.coby.HotSpot.Domain", + deploymentTargets: .iOS("15.0"), + infoPlist: .default, + sources: ["Sources/**"], + dependencies: [ + .project(target: "Data", path: "../Data"), + .project(target: "Shared", path: "../Shared") + ] + ), + .target( + name: "DomainTests", + destinations: [.iPhone], + product: .unitTests, + bundleId: "com.coby.HotSpot.DomainTests", + infoPlist: .default, + sources: ["Tests/**"], + dependencies: [.target(name: "Domain")] + ) + ] +) \ No newline at end of file diff --git a/HotSpot/Domain/README.md b/HotSpot/Domain/README.md new file mode 100644 index 0000000..7025cc9 --- /dev/null +++ b/HotSpot/Domain/README.md @@ -0,0 +1,21 @@ +# Domain モジュール + +## 概要 +Domainモジュールは、アプリケーションのビジネスロジックとドメインモデルを定義します。 + +## 構造 +``` +Domain/ +├── Sources/ +│ ├── Entity/ # ドメインモデル +│ ├── Repository/ # リポジトリインターフェース +│ └── UseCase/ # ビジネスロジック +└── Tests/ # ユニットテスト +``` + +## 依存関係 +- Coreモジュール + +## 使用方法 +Domainモジュールは、アプリケーションのビジネスルールとドメインロジックをカプセル化します。 +DataモジュールとPresentationモジュールから参照されます。 \ No newline at end of file diff --git a/HotSpot/Sources/Common/Dependency/ShopRepository+Dependency.swift b/HotSpot/Domain/Sources/Dependency/ShopRepository+Dependency.swift similarity index 61% rename from HotSpot/Sources/Common/Dependency/ShopRepository+Dependency.swift rename to HotSpot/Domain/Sources/Dependency/ShopRepository+Dependency.swift index 9af0c58..2de21b1 100644 --- a/HotSpot/Sources/Common/Dependency/ShopRepository+Dependency.swift +++ b/HotSpot/Domain/Sources/Dependency/ShopRepository+Dependency.swift @@ -1,13 +1,14 @@ import Foundation import ComposableArchitecture +import Data -private enum ShopRepositoryKey: DependencyKey { - static let liveValue: ShopRepository = ShopRepositoryImpl( +public enum ShopRepositoryKey: DependencyKey { + public static let liveValue: ShopRepository = ShopRepositoryImpl( remoteDataSource: ShopRemoteDataSourceImpl() ) } -extension DependencyValues { +public extension DependencyValues { var shopRepository: ShopRepository { get { self[ShopRepositoryKey.self] } set { self[ShopRepositoryKey.self] = newValue } diff --git a/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift b/HotSpot/Domain/Sources/Dependency/UserDefaults+Dependency.swift similarity index 97% rename from HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift rename to HotSpot/Domain/Sources/Dependency/UserDefaults+Dependency.swift index 176e72f..789e1c5 100644 --- a/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift +++ b/HotSpot/Domain/Sources/Dependency/UserDefaults+Dependency.swift @@ -5,7 +5,7 @@ private enum UserDefaultsKey: DependencyKey { static let liveValue = UserDefaults.standard } -extension DependencyValues { +public extension DependencyValues { var userDefaults: UserDefaults { get { self[UserDefaultsKey.self] } set { self[UserDefaultsKey.self] = newValue } @@ -13,7 +13,7 @@ extension DependencyValues { } // MARK: - Filter Keys -extension UserDefaults { +public extension UserDefaults { private enum FilterKey: String { case range = "range" case budgets = "budgets" diff --git a/HotSpot/Sources/Domain/Model/Filter/Budget.swift b/HotSpot/Domain/Sources/Entity/Filter/Budget.swift similarity index 100% rename from HotSpot/Sources/Domain/Model/Filter/Budget.swift rename to HotSpot/Domain/Sources/Entity/Filter/Budget.swift diff --git a/HotSpot/Sources/Domain/Model/Filter/Feature.swift b/HotSpot/Domain/Sources/Entity/Filter/Feature.swift similarity index 73% rename from HotSpot/Sources/Domain/Model/Filter/Feature.swift rename to HotSpot/Domain/Sources/Entity/Filter/Feature.swift index c2917ce..c72021f 100644 --- a/HotSpot/Sources/Domain/Model/Filter/Feature.swift +++ b/HotSpot/Domain/Sources/Entity/Filter/Feature.swift @@ -1,12 +1,12 @@ import Foundation -enum Feature: String, CaseIterable { +public enum Feature: String, CaseIterable { case wifi = "Wi-Fiあり" case privateRoom = "個室あり" case nonSmoking = "禁煙席あり" case parking = "駐車場あり" - var name: String { + public var name: String { return self.rawValue } } \ No newline at end of file diff --git a/HotSpot/Sources/Domain/Model/Filter/Genre.swift b/HotSpot/Domain/Sources/Entity/Filter/Genre.swift similarity index 100% rename from HotSpot/Sources/Domain/Model/Filter/Genre.swift rename to HotSpot/Domain/Sources/Entity/Filter/Genre.swift diff --git a/HotSpot/Sources/Domain/Model/Filter/Range.swift b/HotSpot/Domain/Sources/Entity/Filter/Range.swift similarity index 86% rename from HotSpot/Sources/Domain/Model/Filter/Range.swift rename to HotSpot/Domain/Sources/Entity/Filter/Range.swift index f59cf64..8b79763 100644 --- a/HotSpot/Sources/Domain/Model/Filter/Range.swift +++ b/HotSpot/Domain/Sources/Entity/Filter/Range.swift @@ -1,13 +1,13 @@ import Foundation -enum Range: Int, CaseIterable { +public enum Range: Int, CaseIterable { case threeHundredMeters = 1 case fiveHundredMeters = 2 case oneKilometer = 3 case twoKilometers = 4 case threeKilometers = 5 - var name: String { + public var name: String { switch self { case .threeHundredMeters: return "300m" case .fiveHundredMeters: return "500m" diff --git a/HotSpot/Sources/Domain/Model/MapCoordinate.swift b/HotSpot/Domain/Sources/Entity/MapCoordinate.swift similarity index 54% rename from HotSpot/Sources/Domain/Model/MapCoordinate.swift rename to HotSpot/Domain/Sources/Entity/MapCoordinate.swift index 21a790e..960b83a 100644 --- a/HotSpot/Sources/Domain/Model/MapCoordinate.swift +++ b/HotSpot/Domain/Sources/Entity/MapCoordinate.swift @@ -2,40 +2,40 @@ import Foundation import CoreLocation import MapKit -struct MapCoordinate: Equatable { - let latitude: Double - let longitude: Double +public struct MapCoordinate: Equatable { + public let latitude: Double + public let longitude: Double - init(latitude: Double, longitude: Double) { + public init(latitude: Double, longitude: Double) { self.latitude = latitude self.longitude = longitude } - init(coordinate: CLLocationCoordinate2D) { + public init(coordinate: CLLocationCoordinate2D) { self.latitude = coordinate.latitude self.longitude = coordinate.longitude } - var clLocationCoordinate2D: CLLocationCoordinate2D { + public var clLocationCoordinate2D: CLLocationCoordinate2D { CLLocationCoordinate2D(latitude: latitude, longitude: longitude) } } -struct MapRegion: Equatable { - let center: MapCoordinate - let span: MapSpan +public struct MapRegion: Equatable { + public let center: MapCoordinate + public let span: MapSpan - init(center: MapCoordinate, span: MapSpan) { + public init(center: MapCoordinate, span: MapSpan) { self.center = center self.span = span } - init(region: MKCoordinateRegion) { + public init(region: MKCoordinateRegion) { self.center = MapCoordinate(coordinate: region.center) self.span = MapSpan(span: region.span) } - var mkCoordinateRegion: MKCoordinateRegion { + public var mkCoordinateRegion: MKCoordinateRegion { MKCoordinateRegion( center: center.clLocationCoordinate2D, span: span.mkCoordinateSpan @@ -43,21 +43,21 @@ struct MapRegion: Equatable { } } -struct MapSpan: Equatable { - let latitudeDelta: Double - let longitudeDelta: Double +public struct MapSpan: Equatable { + public let latitudeDelta: Double + public let longitudeDelta: Double - init(latitudeDelta: Double, longitudeDelta: Double) { + public init(latitudeDelta: Double, longitudeDelta: Double) { self.latitudeDelta = latitudeDelta self.longitudeDelta = longitudeDelta } - init(span: MKCoordinateSpan) { + public init(span: MKCoordinateSpan) { self.latitudeDelta = span.latitudeDelta self.longitudeDelta = span.longitudeDelta } - var mkCoordinateSpan: MKCoordinateSpan { + public var mkCoordinateSpan: MKCoordinateSpan { MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta) } } \ No newline at end of file diff --git a/HotSpot/Sources/Domain/Model/SearchResultModel.swift b/HotSpot/Domain/Sources/Entity/SearchResultModel.swift similarity index 57% rename from HotSpot/Sources/Domain/Model/SearchResultModel.swift rename to HotSpot/Domain/Sources/Entity/SearchResultModel.swift index 10f0bbc..7952cb0 100644 --- a/HotSpot/Sources/Domain/Model/SearchResultModel.swift +++ b/HotSpot/Domain/Sources/Entity/SearchResultModel.swift @@ -1,20 +1,21 @@ import Foundation +import Data -struct SearchResultModel { - let shops: [ShopModel] - let currentPage: Int - let hasMore: Bool - let totalCount: Int +public struct SearchResultModel { + public let shops: [ShopModel] + public let currentPage: Int + public let hasMore: Bool + public let totalCount: Int - init(shops: [ShopModel], currentPage: Int, hasMore: Bool, totalCount: Int) { + public init(shops: [ShopModel], currentPage: Int, hasMore: Bool, totalCount: Int) { self.shops = shops self.currentPage = currentPage self.hasMore = hasMore self.totalCount = totalCount } - static func from(response: ShopSearchResponseDTO, currentPage: Int) -> SearchResultModel { - let shops = response.results.shop.map { $0.toDomain() } + public static func from(response: ShopSearchResponseDTO, currentPage: Int) -> SearchResultModel { + let shops = response.results.shop.map { $0.toShopModel() } let resultsReturned = Int(response.results.resultsReturned) ?? 1 let currentEnd = response.results.resultsStart + resultsReturned let hasMore = currentEnd < response.results.resultsAvailable @@ -26,4 +27,4 @@ struct SearchResultModel { totalCount: response.results.resultsAvailable ) } -} \ No newline at end of file +} diff --git a/HotSpot/Sources/Domain/Error/ShopError.swift b/HotSpot/Domain/Sources/Entity/ShopError.swift similarity index 71% rename from HotSpot/Sources/Domain/Error/ShopError.swift rename to HotSpot/Domain/Sources/Entity/ShopError.swift index c7026b5..fae5c69 100644 --- a/HotSpot/Sources/Domain/Error/ShopError.swift +++ b/HotSpot/Domain/Sources/Entity/ShopError.swift @@ -1,6 +1,6 @@ import Foundation -enum ShopError: Error, Equatable { +public enum ShopError: Error, Equatable { case network case decoding case server(message: String) diff --git a/HotSpot/Domain/Sources/Entity/ShopModel.swift b/HotSpot/Domain/Sources/Entity/ShopModel.swift new file mode 100644 index 0000000..d10f645 --- /dev/null +++ b/HotSpot/Domain/Sources/Entity/ShopModel.swift @@ -0,0 +1,71 @@ +import Foundation + +public struct ShopModel: Identifiable, Equatable { + public let id: String + public let name: String + public let address: String + public let latitude: Double + public let longitude: Double + public let imageUrl: String + public let access: String + public let openingHours: String + public let genre: Genre + public let budget: Budget + public let url: String + public let wifi: Int + public let privateRoom: Int + public let nonSmoking: Int + public let parking: Int + + public var coordinate: MapCoordinate { + MapCoordinate(latitude: latitude, longitude: longitude) + } + + public static func filterVisibleShops(_ shops: [ShopModel], in region: MapRegion) -> [ShopModel] { + shops.filter { shop in + let latMin = region.center.latitude - region.span.latitudeDelta / 2 + let latMax = region.center.latitude + region.span.latitudeDelta / 2 + let lonMin = region.center.longitude - region.span.longitudeDelta / 2 + let lonMax = region.center.longitude + region.span.longitudeDelta / 2 + + return shop.latitude >= latMin && + shop.latitude <= latMax && + shop.longitude >= lonMin && + shop.longitude <= lonMax + } + } + + public init( + id: String, + name: String, + address: String, + latitude: Double, + longitude: Double, + imageUrl: String, + access: String, + openingHours: String, + genre: Genre, + budget: Budget, + url: String, + wifi: Int, + privateRoom: Int, + nonSmoking: Int, + parking: Int + ) { + self.id = id + self.name = name + self.address = address + self.latitude = latitude + self.longitude = longitude + self.imageUrl = imageUrl + self.access = access + self.openingHours = openingHours + self.genre = genre + self.budget = budget + self.url = url + self.wifi = wifi + self.privateRoom = privateRoom + self.nonSmoking = nonSmoking + self.parking = parking + } +} diff --git a/HotSpot/Domain/Sources/Mapper/ShopMapper.swift b/HotSpot/Domain/Sources/Mapper/ShopMapper.swift new file mode 100644 index 0000000..1cea059 --- /dev/null +++ b/HotSpot/Domain/Sources/Mapper/ShopMapper.swift @@ -0,0 +1,36 @@ +import Foundation +import Data + +extension ShopDTO.Genre { + func toGenre() -> Genre? { + return Genre(rawValue: code) + } +} + +extension ShopDTO.Budget { + func toBudget() -> Budget? { + return Budget(rawValue: code) + } +} + +extension ShopDTO { + func toShopModel() -> ShopModel { + ShopModel( + id: id, + name: name, + address: address, + latitude: lat, + longitude: lng, + imageUrl: photo.pc.large, + access: access, + openingHours: open ?? "営業時間情報なし", + genre: genre.toGenre() ?? .other, + budget: budget?.toBudget() ?? .from1501to2000, + url: urls.pc, + wifi: wifi == "あり" ? 1 : 0, + privateRoom: privateRoom == "あり" ? 1 : 0, + nonSmoking: nonSmoking == "あり" ? 1 : 0, + parking: parking == "あり" ? 1 : 0 + ) + } +} diff --git a/HotSpot/Sources/Domain/Repository/ShopRepository.swift b/HotSpot/Domain/Sources/Repository/ShopRepository.swift similarity index 71% rename from HotSpot/Sources/Domain/Repository/ShopRepository.swift rename to HotSpot/Domain/Sources/Repository/ShopRepository.swift index 512a093..252209b 100644 --- a/HotSpot/Sources/Domain/Repository/ShopRepository.swift +++ b/HotSpot/Domain/Sources/Repository/ShopRepository.swift @@ -1,5 +1,6 @@ import Foundation +import Data -protocol ShopRepository { +public protocol ShopRepository { func searchShops(request: ShopSearchRequestDTO) async throws -> ShopSearchResponseDTO } diff --git a/HotSpot/Domain/Sources/Repository/ShopRepositoryImpl.swift b/HotSpot/Domain/Sources/Repository/ShopRepositoryImpl.swift new file mode 100644 index 0000000..6fa5ff7 --- /dev/null +++ b/HotSpot/Domain/Sources/Repository/ShopRepositoryImpl.swift @@ -0,0 +1,14 @@ +import Foundation +import Data + +public final class ShopRepositoryImpl: ShopRepository { + private let remoteDataSource: ShopRemoteDataSource + + public init(remoteDataSource: ShopRemoteDataSource) { + self.remoteDataSource = remoteDataSource + } + + public func searchShops(request: ShopSearchRequestDTO) async throws -> ShopSearchResponseDTO { + try await remoteDataSource.search(request: request) + } +} diff --git a/HotSpot/Domain/Sources/UseCase/InfiniteScrollSearchUseCase.swift b/HotSpot/Domain/Sources/UseCase/InfiniteScrollSearchUseCase.swift new file mode 100644 index 0000000..febfbd1 --- /dev/null +++ b/HotSpot/Domain/Sources/UseCase/InfiniteScrollSearchUseCase.swift @@ -0,0 +1,48 @@ +import Foundation +import Data + +public struct InfiniteScrollSearchUseCase { + private let repository: ShopRepository + private let pageSize: Int + + public init(repository: ShopRepository, pageSize: Int = 20) { + self.repository = repository + self.pageSize = pageSize + } + + public func execute( + latitude: Double, + longitude: Double, + range: Int, + name: String?, + genres: [String]?, + budgets: [String]?, + privateRoom: Int, + wifi: Int, + nonSmoking: Int, + parking: Int, + currentPage: Int, + isLoadMore: Bool = false + ) async throws -> SearchResultModel { + let targetPage = isLoadMore ? currentPage + 1 : currentPage + let start = (targetPage - 1) * pageSize + 1 + + let request = ShopSearchRequestDTO( + lat: latitude, + lng: longitude, + range: range, + count: pageSize, + name: name, + genres: genres, + start: start, + budgets: budgets, + privateRoom: privateRoom, + wifi: wifi, + nonSmoking: nonSmoking, + parking: parking + ) + + let response = try await repository.searchShops(request: request) + return SearchResultModel.from(response: response, currentPage: targetPage) + } +} diff --git a/HotSpot/Sources/Domain/Service/LocationManager.swift b/HotSpot/Domain/Sources/UseCase/LocationManager.swift similarity index 84% rename from HotSpot/Sources/Domain/Service/LocationManager.swift rename to HotSpot/Domain/Sources/UseCase/LocationManager.swift index 652894a..c7ff39b 100644 --- a/HotSpot/Sources/Domain/Service/LocationManager.swift +++ b/HotSpot/Domain/Sources/UseCase/LocationManager.swift @@ -3,12 +3,12 @@ import CoreLocation import ComposableArchitecture import Dependencies -struct LocationManager: DependencyKey { - static var liveValue: LocationManager = .live +public struct LocationManager: DependencyKey { + public static var liveValue: LocationManager = .live - var requestLocation: @Sendable () async -> CLLocation? + public var requestLocation: @Sendable () async -> CLLocation? - static let live = Self( + public static let live = Self( requestLocation: { let manager = CLLocationManager() manager.requestWhenInUseAuthorization() @@ -41,7 +41,7 @@ private class LocationDelegate: NSObject, CLLocationManagerDelegate { } } -extension DependencyValues { +public extension DependencyValues { var locationManager: LocationManager { get { self[LocationManager.self] } set { self[LocationManager.self] = newValue } diff --git a/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift b/HotSpot/Domain/Sources/UseCase/ShopsUseCase.swift similarity index 70% rename from HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift rename to HotSpot/Domain/Sources/UseCase/ShopsUseCase.swift index 7ffc2b9..2094815 100644 --- a/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift +++ b/HotSpot/Domain/Sources/UseCase/ShopsUseCase.swift @@ -1,13 +1,14 @@ import Foundation +import Data -struct ShopsUseCase { +public struct ShopsUseCase { private let repository: ShopRepository - init(repository: ShopRepository) { + public init(repository: ShopRepository) { self.repository = repository } - func execute(lat: Double, lng: Double) async throws -> [ShopModel] { + public func execute(lat: Double, lng: Double) async throws -> [ShopModel] { let request = ShopSearchRequestDTO( lat: lat, lng: lng, @@ -24,6 +25,6 @@ struct ShopsUseCase { ) let response = try await repository.searchShops(request: request) - return response.results.shop.map { $0.toDomain() } + return response.results.shop.map { $0.toShopModel() } } } diff --git a/HotSpot/Domain/Tests/DomainTests.swift b/HotSpot/Domain/Tests/DomainTests.swift new file mode 100644 index 0000000..910b4e6 --- /dev/null +++ b/HotSpot/Domain/Tests/DomainTests.swift @@ -0,0 +1,10 @@ +import XCTest +@testable import Domain + +final class DomainTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + XCTAssertTrue(true) + } +} \ No newline at end of file diff --git a/HotSpot/Presentation/Project.swift b/HotSpot/Presentation/Project.swift new file mode 100644 index 0000000..fe0e67d --- /dev/null +++ b/HotSpot/Presentation/Project.swift @@ -0,0 +1,32 @@ +import ProjectDescription + +let project = Project( + name: "Presentation", + organizationName: "Coby", + targets: [ + .target( + name: "Presentation", + destinations: [.iPhone], + product: .framework, + bundleId: "com.coby.HotSpot.Presentation", + deploymentTargets: .iOS("15.0"), + infoPlist: .default, + sources: ["Sources/**"], + dependencies: [ + .project(target: "Domain", path: "../Domain"), + .project(target: "Shared", path: "../Shared"), + .external(name: "CobyDS"), + .external(name: "Kingfisher") + ] + ), + .target( + name: "PresentationTests", + destinations: [.iPhone], + product: .unitTests, + bundleId: "com.coby.HotSpot.PresentationTests", + infoPlist: .default, + sources: ["Tests/**"], + dependencies: [.target(name: "Presentation")] + ) + ] +) \ No newline at end of file diff --git a/HotSpot/Presentation/README.md b/HotSpot/Presentation/README.md new file mode 100644 index 0000000..df54481 --- /dev/null +++ b/HotSpot/Presentation/README.md @@ -0,0 +1,32 @@ +# Presentation モジュール + +## 概要 +Presentationモジュールは、ユーザーインターフェースと画面遷移を担当します。 + +## 構造 +``` +Presentation/ +├── Sources/ +│ ├── Search/ # 検索機能 +│ │ ├── View/ # ビュー +│ │ ├── Store/ # 状態管理 +│ │ └── Component/ # UIコンポーネント +│ ├── Map/ # 地図機能 +│ │ ├── View/ +│ │ ├── Store/ +│ │ └── Component/ +│ └── ShopDetail/ # 店舗詳細 +│ ├── View/ +│ ├── Store/ +│ └── Component/ +└── Tests/ # ユニットテスト +``` + +## 依存関係 +- Domainモジュール +- CobyDS +- Kingfisher + +## 使用方法 +Presentationモジュールは、ユーザーインターフェースの表示とユーザー操作の処理を担当します。 +Domainモジュールのユースケースを使用してビジネスロジックを実行します。 \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift b/HotSpot/Presentation/Sources/Common/Coordinator/AppCoordinator.swift similarity index 87% rename from HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift rename to HotSpot/Presentation/Sources/Common/Coordinator/AppCoordinator.swift index 332d314..70855f8 100644 --- a/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift +++ b/HotSpot/Presentation/Sources/Common/Coordinator/AppCoordinator.swift @@ -1,19 +1,20 @@ import SwiftUI import UIKit import ComposableArchitecture +import Domain -final class AppCoordinator { +public final class AppCoordinator { private let window: UIWindow private let navigationController: UINavigationController private var errorAlertController: UIAlertController? private var messageAlertController: UIAlertController? - init(window: UIWindow) { + public init(window: UIWindow) { self.window = window self.navigationController = CustomNavigationController() } - func start() { + public func start() { let mapView = MapView( store: Store( initialState: MapStore.State(), @@ -29,12 +30,12 @@ final class AppCoordinator { window.makeKeyAndVisible() } - func showError(_ error: ShopError) { + public func showError(_ error: ShopError) { let errorMessage = ShopErrorMessageMapper.message(for: error) showAlert(title: "エラー", message: errorMessage) } - func showMessage(title: String, message: String) { + public func showMessage(title: String, message: String) { showAlert(title: title, message: message) } @@ -67,7 +68,7 @@ final class AppCoordinator { navigationController.present(alert, animated: true) } - func showSearch() { + public func showSearch() { let searchView = SearchView( store: Store( initialState: SearchStore.State(), @@ -79,7 +80,7 @@ final class AppCoordinator { push(searchView) } - func showSearchFilter() { + public func showSearchFilter() { let searchFilterView = SearchFilterView( store: Store( initialState: SearchFilterStore.State(), @@ -91,7 +92,7 @@ final class AppCoordinator { push(searchFilterView) } - func showShopDetail(_ shop: ShopModel) { + public func showShopDetail(_ shop: ShopModel) { let shopDetailView = ShopDetailView( store: Store( initialState: ShopDetailStore.State(shop: shop), @@ -108,11 +109,11 @@ final class AppCoordinator { navigationController.pushViewController(hostingController, animated: animated) } - func pop(animated: Bool = true) { + public func pop(animated: Bool = true) { navigationController.popViewController(animated: animated) } - func popToRoot(animated: Bool = true) { + public func popToRoot(animated: Bool = true) { navigationController.popToRootViewController(animated: animated) } } diff --git a/HotSpot/Sources/Presentation/Coordinator/CustomNavigationController.swift b/HotSpot/Presentation/Sources/Common/Coordinator/CustomNavigationController.swift similarity index 100% rename from HotSpot/Sources/Presentation/Coordinator/CustomNavigationController.swift rename to HotSpot/Presentation/Sources/Common/Coordinator/CustomNavigationController.swift diff --git a/HotSpot/Sources/Presentation/Coordinator/NavigationConfig.swift b/HotSpot/Presentation/Sources/Common/Coordinator/NavigationConfig.swift similarity index 96% rename from HotSpot/Sources/Presentation/Coordinator/NavigationConfig.swift rename to HotSpot/Presentation/Sources/Common/Coordinator/NavigationConfig.swift index d57c4b4..4d76614 100644 --- a/HotSpot/Sources/Presentation/Coordinator/NavigationConfig.swift +++ b/HotSpot/Presentation/Sources/Common/Coordinator/NavigationConfig.swift @@ -1,7 +1,7 @@ import SwiftUI import UIKit -extension UINavigationBar { +public extension UINavigationBar { static func configureAppearance() { let appearance = UINavigationBarAppearance() appearance.configureWithTransparentBackground() diff --git a/HotSpot/Sources/Presentation/Common/ErrorMapper/ShopErrorMessageMapper.swift b/HotSpot/Presentation/Sources/Common/ShopErrorMessageMapper.swift similarity index 97% rename from HotSpot/Sources/Presentation/Common/ErrorMapper/ShopErrorMessageMapper.swift rename to HotSpot/Presentation/Sources/Common/ShopErrorMessageMapper.swift index f37784c..86b4de0 100644 --- a/HotSpot/Sources/Presentation/Common/ErrorMapper/ShopErrorMessageMapper.swift +++ b/HotSpot/Presentation/Sources/Common/ShopErrorMessageMapper.swift @@ -1,4 +1,5 @@ import Foundation +import Domain struct ShopErrorMessageMapper { static func message(for error: ShopError) -> String { diff --git a/HotSpot/Sources/Presentation/Map/Component/CarouselScrollViewRepresentable.swift b/HotSpot/Presentation/Sources/Map/Component/CarouselScrollViewRepresentable.swift similarity index 100% rename from HotSpot/Sources/Presentation/Map/Component/CarouselScrollViewRepresentable.swift rename to HotSpot/Presentation/Sources/Map/Component/CarouselScrollViewRepresentable.swift diff --git a/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift b/HotSpot/Presentation/Sources/Map/Component/MapRepresentableView.swift similarity index 99% rename from HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift rename to HotSpot/Presentation/Sources/Map/Component/MapRepresentableView.swift index 6957216..1da55d0 100644 --- a/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift +++ b/HotSpot/Presentation/Sources/Map/Component/MapRepresentableView.swift @@ -1,6 +1,7 @@ import SwiftUI import MapKit import CoreLocation +import Domain struct MapRepresentableView: UIViewRepresentable { var shops: [ShopModel] diff --git a/HotSpot/Sources/Presentation/Map/Component/ShopAnnotation.swift b/HotSpot/Presentation/Sources/Map/Component/ShopAnnotation.swift similarity index 100% rename from HotSpot/Sources/Presentation/Map/Component/ShopAnnotation.swift rename to HotSpot/Presentation/Sources/Map/Component/ShopAnnotation.swift diff --git a/HotSpot/Sources/Presentation/Map/Component/ShopAnnotationView.swift b/HotSpot/Presentation/Sources/Map/Component/ShopAnnotationView.swift similarity index 98% rename from HotSpot/Sources/Presentation/Map/Component/ShopAnnotationView.swift rename to HotSpot/Presentation/Sources/Map/Component/ShopAnnotationView.swift index 4fa9586..25097cc 100644 --- a/HotSpot/Sources/Presentation/Map/Component/ShopAnnotationView.swift +++ b/HotSpot/Presentation/Sources/Map/Component/ShopAnnotationView.swift @@ -1,5 +1,6 @@ import UIKit import MapKit +import Domain class ShopAnnotationView: MKMarkerAnnotationView { static let reuseIdentifier = "ShopAnnotationView" diff --git a/HotSpot/Sources/Presentation/Map/Component/ShopClusterAnnotationView.swift b/HotSpot/Presentation/Sources/Map/Component/ShopClusterAnnotationView.swift similarity index 100% rename from HotSpot/Sources/Presentation/Map/Component/ShopClusterAnnotationView.swift rename to HotSpot/Presentation/Sources/Map/Component/ShopClusterAnnotationView.swift diff --git a/HotSpot/Sources/Presentation/Map/MapStore.swift b/HotSpot/Presentation/Sources/Map/Store/MapStore.swift similarity index 99% rename from HotSpot/Sources/Presentation/Map/MapStore.swift rename to HotSpot/Presentation/Sources/Map/Store/MapStore.swift index 7ecc8aa..3c386c7 100644 --- a/HotSpot/Sources/Presentation/Map/MapStore.swift +++ b/HotSpot/Presentation/Sources/Map/Store/MapStore.swift @@ -2,6 +2,7 @@ import Foundation import CoreLocation import ComposableArchitecture import MapKit +import Domain struct MapStore: Reducer { @Dependency(\.shopRepository) var shopRepository diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Presentation/Sources/Map/View/MapView.swift similarity index 99% rename from HotSpot/Sources/Presentation/Map/MapView.swift rename to HotSpot/Presentation/Sources/Map/View/MapView.swift index bad31f2..f991493 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Presentation/Sources/Map/View/MapView.swift @@ -3,6 +3,7 @@ import MapKit import ComposableArchitecture import CobyDS import Kingfisher +import Domain struct MapView: View { let store: StoreOf diff --git a/HotSpot/Sources/Presentation/Search/Component/EmptyResults.swift b/HotSpot/Presentation/Sources/Search/Component/EmptyResults.swift similarity index 100% rename from HotSpot/Sources/Presentation/Search/Component/EmptyResults.swift rename to HotSpot/Presentation/Sources/Search/Component/EmptyResults.swift diff --git a/HotSpot/Sources/Presentation/Search/Component/SearchBar.swift b/HotSpot/Presentation/Sources/Search/Component/SearchBar.swift similarity index 100% rename from HotSpot/Sources/Presentation/Search/Component/SearchBar.swift rename to HotSpot/Presentation/Sources/Search/Component/SearchBar.swift diff --git a/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift b/HotSpot/Presentation/Sources/Search/Component/SearchResults.swift similarity index 99% rename from HotSpot/Sources/Presentation/Search/Component/SearchResults.swift rename to HotSpot/Presentation/Sources/Search/Component/SearchResults.swift index 156b04a..5ff74aa 100644 --- a/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift +++ b/HotSpot/Presentation/Sources/Search/Component/SearchResults.swift @@ -1,6 +1,7 @@ import SwiftUI import CobyDS import Kingfisher +import Domain struct SearchResults: View { let error: ShopError? diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Presentation/Sources/Search/Store/SearchStore.swift similarity index 78% rename from HotSpot/Sources/Presentation/Search/SearchStore.swift rename to HotSpot/Presentation/Sources/Search/Store/SearchStore.swift index 630752f..7447060 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Presentation/Sources/Search/Store/SearchStore.swift @@ -1,21 +1,24 @@ import Foundation import CoreLocation import ComposableArchitecture +import Domain @Reducer -struct SearchStore { +public struct SearchStore { @Dependency(\.shopRepository) var shopRepository @Dependency(\.userDefaults) var userDefaults - struct State: Equatable { - var shops: [ShopModel] = [] - var searchText: String = "" - var error: ShopError? = nil - var currentPage: Int = 1 - var isLastPage: Bool = false + public struct State: Equatable { + public var shops: [ShopModel] = [] + public var searchText: String = "" + public var error: ShopError? = nil + public var currentPage: Int = 1 + public var isLastPage: Bool = false + + public init() {} } - enum Action { + public enum Action { case search case updateSearchText(String) case updateShops([ShopModel]) @@ -24,8 +27,10 @@ struct SearchStore { case loadMore case updatePage(Int, Bool) } + + public init() {} - var body: some ReducerOf { + public var body: some ReducerOf { Reduce { state, action in switch action { case let .updateShops(shops): @@ -57,24 +62,18 @@ struct SearchStore { return .run { [state] send in do { - let request = ShopSearchRequestDTO( - lat: userDefaults.location.latitude, - lng: userDefaults.location.longitude, + let useCase = InfiniteScrollSearchUseCase(repository: shopRepository) + let result = try await useCase.execute( + latitude: userDefaults.location.latitude, + longitude: userDefaults.location.longitude, range: userDefaults.range, - count: nil, name: state.searchText.isEmpty ? nil : state.searchText, genres: !userDefaults.genres.isEmpty ? userDefaults.genres : nil, - start: nil, budgets: !userDefaults.budgets.isEmpty ? userDefaults.budgets : nil, privateRoom: userDefaults.privateRoom, wifi: userDefaults.wifi, nonSmoking: userDefaults.nonSmoking, - parking: userDefaults.parking - ) - - let useCase = InfiniteScrollSearchUseCase(repository: shopRepository) - let result = try await useCase.execute( - request: request, + parking: userDefaults.parking, currentPage: 1 ) @@ -91,24 +90,18 @@ struct SearchStore { return .run { [state] send in do { let location = userDefaults.location - let request = ShopSearchRequestDTO( - lat: location.latitude, - lng: location.longitude, + let useCase = InfiniteScrollSearchUseCase(repository: shopRepository) + let result = try await useCase.execute( + latitude: location.latitude, + longitude: location.longitude, range: userDefaults.range, - count: nil, name: state.searchText.isEmpty ? nil : state.searchText, genres: !userDefaults.genres.isEmpty ? userDefaults.genres : nil, - start: nil, budgets: !userDefaults.budgets.isEmpty ? userDefaults.budgets : nil, privateRoom: userDefaults.privateRoom, wifi: userDefaults.wifi, nonSmoking: userDefaults.nonSmoking, - parking: userDefaults.parking - ) - - let useCase = InfiniteScrollSearchUseCase(repository: shopRepository) - let result = try await useCase.execute( - request: request, + parking: userDefaults.parking, currentPage: state.currentPage, isLoadMore: true ) diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Presentation/Sources/Search/View/SearchView.swift similarity index 100% rename from HotSpot/Sources/Presentation/Search/SearchView.swift rename to HotSpot/Presentation/Sources/Search/View/SearchView.swift diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift b/HotSpot/Presentation/Sources/SearchFilter/Component/BudgetSection.swift similarity index 98% rename from HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift rename to HotSpot/Presentation/Sources/SearchFilter/Component/BudgetSection.swift index 0ce29e9..b692b8f 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift +++ b/HotSpot/Presentation/Sources/SearchFilter/Component/BudgetSection.swift @@ -1,5 +1,6 @@ import SwiftUI import CobyDS +import Domain struct BudgetSection: View { let selectedBudgets: [String] diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift b/HotSpot/Presentation/Sources/SearchFilter/Component/FeaturesSection.swift similarity index 98% rename from HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift rename to HotSpot/Presentation/Sources/SearchFilter/Component/FeaturesSection.swift index 0a1346d..64ebab3 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift +++ b/HotSpot/Presentation/Sources/SearchFilter/Component/FeaturesSection.swift @@ -1,5 +1,6 @@ import SwiftUI import CobyDS +import Domain struct FeaturesSection: View { let selectedFeatures: Set diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/FilterButton.swift b/HotSpot/Presentation/Sources/SearchFilter/Component/FilterButton.swift similarity index 100% rename from HotSpot/Sources/Presentation/SearchFilter/Component/FilterButton.swift rename to HotSpot/Presentation/Sources/SearchFilter/Component/FilterButton.swift diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift b/HotSpot/Presentation/Sources/SearchFilter/Component/GenreSection.swift similarity index 98% rename from HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift rename to HotSpot/Presentation/Sources/SearchFilter/Component/GenreSection.swift index 8af19c7..fc3c9d9 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift +++ b/HotSpot/Presentation/Sources/SearchFilter/Component/GenreSection.swift @@ -1,5 +1,6 @@ import SwiftUI import CobyDS +import Domain struct GenreSection: View { let selectedGenres: [String] diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift b/HotSpot/Presentation/Sources/SearchFilter/Component/RangeSection.swift similarity index 98% rename from HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift rename to HotSpot/Presentation/Sources/SearchFilter/Component/RangeSection.swift index e426a9d..685056e 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift +++ b/HotSpot/Presentation/Sources/SearchFilter/Component/RangeSection.swift @@ -1,5 +1,6 @@ import SwiftUI import CobyDS +import Domain struct RangeSection: View { let selectedRange: Int diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift b/HotSpot/Presentation/Sources/SearchFilter/Store/SearchFilterStore.swift similarity index 99% rename from HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift rename to HotSpot/Presentation/Sources/SearchFilter/Store/SearchFilterStore.swift index ac153d2..4d97842 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift +++ b/HotSpot/Presentation/Sources/SearchFilter/Store/SearchFilterStore.swift @@ -1,5 +1,6 @@ import Foundation import ComposableArchitecture +import Domain @Reducer struct SearchFilterStore { diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift b/HotSpot/Presentation/Sources/SearchFilter/View/SearchFilterView.swift similarity index 99% rename from HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift rename to HotSpot/Presentation/Sources/SearchFilter/View/SearchFilterView.swift index 6bded3d..9ffc4e9 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift +++ b/HotSpot/Presentation/Sources/SearchFilter/View/SearchFilterView.swift @@ -1,6 +1,7 @@ import SwiftUI import ComposableArchitecture import CobyDS +import Domain struct SearchFilterView: View { let store: StoreOf diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/FacilityIcon.swift b/HotSpot/Presentation/Sources/ShopDetail/Component/FacilityIcon.swift similarity index 100% rename from HotSpot/Sources/Presentation/ShopDetail/Component/FacilityIcon.swift rename to HotSpot/Presentation/Sources/ShopDetail/Component/FacilityIcon.swift diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/InfoRow.swift b/HotSpot/Presentation/Sources/ShopDetail/Component/InfoRow.swift similarity index 100% rename from HotSpot/Sources/Presentation/ShopDetail/Component/InfoRow.swift rename to HotSpot/Presentation/Sources/ShopDetail/Component/InfoRow.swift diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift b/HotSpot/Presentation/Sources/ShopDetail/Component/ShopDetailSection.swift similarity index 99% rename from HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift rename to HotSpot/Presentation/Sources/ShopDetail/Component/ShopDetailSection.swift index 1b3d6bd..b25948c 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift +++ b/HotSpot/Presentation/Sources/ShopDetail/Component/ShopDetailSection.swift @@ -1,5 +1,6 @@ import SwiftUI import CobyDS +import Domain struct ShopDetailSection: View { let shop: ShopModel diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopImageSection.swift b/HotSpot/Presentation/Sources/ShopDetail/Component/ShopImageSection.swift similarity index 100% rename from HotSpot/Sources/Presentation/ShopDetail/Component/ShopImageSection.swift rename to HotSpot/Presentation/Sources/ShopDetail/Component/ShopImageSection.swift diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift b/HotSpot/Presentation/Sources/ShopDetail/Component/ShopLocationMapView.swift similarity index 99% rename from HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift rename to HotSpot/Presentation/Sources/ShopDetail/Component/ShopLocationMapView.swift index 5469c15..a0c5976 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift +++ b/HotSpot/Presentation/Sources/ShopDetail/Component/ShopLocationMapView.swift @@ -1,5 +1,6 @@ import SwiftUI import MapKit +import Domain struct ShopLocationMapView: View { let shop: ShopModel diff --git a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailStore.swift b/HotSpot/Presentation/Sources/ShopDetail/Store/ShopDetailStore.swift similarity index 96% rename from HotSpot/Sources/Presentation/ShopDetail/ShopDetailStore.swift rename to HotSpot/Presentation/Sources/ShopDetail/Store/ShopDetailStore.swift index 2b8cb87..8ec2d14 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailStore.swift +++ b/HotSpot/Presentation/Sources/ShopDetail/Store/ShopDetailStore.swift @@ -1,5 +1,6 @@ import Foundation import ComposableArchitecture +import Domain struct ShopDetailStore: Reducer { struct State: Equatable { diff --git a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift b/HotSpot/Presentation/Sources/ShopDetail/View/ShopDetailView.swift similarity index 99% rename from HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift rename to HotSpot/Presentation/Sources/ShopDetail/View/ShopDetailView.swift index e99d77b..fdb5dd4 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift +++ b/HotSpot/Presentation/Sources/ShopDetail/View/ShopDetailView.swift @@ -2,6 +2,7 @@ import SwiftUI import ComposableArchitecture import CobyDS import Kingfisher +import Domain struct ShopDetailView: View { let store: StoreOf diff --git a/HotSpot/Presentation/Tests/PresentationTests.swift b/HotSpot/Presentation/Tests/PresentationTests.swift new file mode 100644 index 0000000..ee80905 --- /dev/null +++ b/HotSpot/Presentation/Tests/PresentationTests.swift @@ -0,0 +1,10 @@ +import XCTest +@testable import Presentation + +final class PresentationTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + XCTAssertTrue(true) + } +} \ No newline at end of file diff --git a/HotSpot/Shared/Project.swift b/HotSpot/Shared/Project.swift new file mode 100644 index 0000000..07c8434 --- /dev/null +++ b/HotSpot/Shared/Project.swift @@ -0,0 +1,29 @@ +import ProjectDescription + +let project = Project( + name: "Shared", + organizationName: "Coby", + targets: [ + .target( + name: "Shared", + destinations: [.iPhone], + product: .framework, + bundleId: "com.coby.HotSpot.Shared", + deploymentTargets: .iOS("15.0"), + infoPlist: .default, + sources: ["Sources/**"], + dependencies: [ + .external(name: "ComposableArchitecture") + ] + ), + .target( + name: "SharedTests", + destinations: [.iPhone], + product: .unitTests, + bundleId: "com.coby.HotSpot.SharedTests", + infoPlist: .default, + sources: ["Tests/**"], + dependencies: [.target(name: "Shared")] + ) + ] +) \ No newline at end of file diff --git a/HotSpot/Shared/README.md b/HotSpot/Shared/README.md new file mode 100644 index 0000000..39961da --- /dev/null +++ b/HotSpot/Shared/README.md @@ -0,0 +1,21 @@ +# Shared モジュール + +## 概要 +Sharedモジュールは、アプリケーションの共通機能とコンポーネントを提供します。 + +## 構造 +``` +Shared/ +├── Sources/ +│ ├── Common/ # 共通で使用されるコンポーネント +│ ├── Extensions/ # Swift基本型の拡張 +│ ├── Utils/ # ユーティリティ関数 +│ └── Dependency/ # 依存性注入 +└── Tests/ # ユニットテスト +``` + +## 依存関係 +- ComposableArchitecture + +## 使用方法 +Sharedモジュールは、他のすべてのモジュールで使用できる共通機能を提供します. \ No newline at end of file diff --git a/HotSpot/Sources/Common/Extensions/MoyaProvider+Extension.swift b/HotSpot/Shared/Sources/Extensions/MoyaProvider+Extension.swift similarity index 91% rename from HotSpot/Sources/Common/Extensions/MoyaProvider+Extension.swift rename to HotSpot/Shared/Sources/Extensions/MoyaProvider+Extension.swift index 6c64823..5f5e479 100644 --- a/HotSpot/Sources/Common/Extensions/MoyaProvider+Extension.swift +++ b/HotSpot/Shared/Sources/Extensions/MoyaProvider+Extension.swift @@ -1,6 +1,6 @@ import Moya -extension MoyaProvider { +public extension MoyaProvider { static var `default`: MoyaProvider { return MoyaProvider( plugins: [NetworkLoggerPlugin(configuration: .init(logOptions: .verbose))] @@ -8,7 +8,7 @@ extension MoyaProvider { } } -extension MoyaProvider { +public extension MoyaProvider { func asyncRequest(_ target: Target) async throws -> Response { return try await withCheckedThrowingContinuation { continuation in self.request(target) { result in diff --git a/HotSpot/Sources/Common/Extensions/UIImage+Kingfisher.swift b/HotSpot/Shared/Sources/Extensions/UIImage+Kingfisher.swift similarity index 98% rename from HotSpot/Sources/Common/Extensions/UIImage+Kingfisher.swift rename to HotSpot/Shared/Sources/Extensions/UIImage+Kingfisher.swift index 945e83a..9198efd 100644 --- a/HotSpot/Sources/Common/Extensions/UIImage+Kingfisher.swift +++ b/HotSpot/Shared/Sources/Extensions/UIImage+Kingfisher.swift @@ -1,7 +1,7 @@ import UIKit import Kingfisher -extension UIImage { +public extension UIImage { static func load(from urlString: String?, completion: @escaping (UIImage?) -> Void) { guard let urlString = urlString, let url = URL(string: urlString) else { diff --git a/HotSpot/Shared/Tests/SharedTests.swift b/HotSpot/Shared/Tests/SharedTests.swift new file mode 100644 index 0000000..faea14f --- /dev/null +++ b/HotSpot/Shared/Tests/SharedTests.swift @@ -0,0 +1,10 @@ +import XCTest +@testable import Shared + +final class SharedTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + XCTAssertTrue(true) + } +} \ No newline at end of file diff --git a/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift b/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift deleted file mode 100644 index 7f91415..0000000 --- a/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation - -struct ShopSearchRequestDTO { - let lat: Double // Latitude - let lng: Double // Longitude - let range: Int // Search range (1–5) - let count: Int? // Number of results (1–100) - let name: String? // Name search - let genres: [String]? // Genre codes - let start: Int? // Starting index for paging - let budgets: [String]? // Budget codes - let privateRoom: Int // Private room availability (0 or 1) - let wifi: Int // Wi-Fi availability (0 or 1) - let nonSmoking: Int // Non-smoking availability (0 or 1) - let parking: Int // Parking availability (0 or 1) - - /// Converts the DTO into a dictionary of parameters for Moya or URL encoding - var asParameters: [String: Any] { - var params: [String: Any] = [ - "lat": lat, - "lng": lng, - "range": range, - "private_room": privateRoom, - "wifi": wifi, - "non_smoking": nonSmoking, - "parking": parking - ] - - if let count = count { params["count"] = count } - if let name = name { params["name"] = name } - if let genres = genres, !genres.isEmpty { params["genre"] = genres.joined(separator: ",") } - if let start = start { params["start"] = start } - if let budgets = budgets, !budgets.isEmpty { params["budget"] = budgets.joined(separator: ",") } - - return params - } -} diff --git a/HotSpot/Sources/Data/DTO/Response/ShopDTO.swift b/HotSpot/Sources/Data/DTO/Response/ShopDTO.swift deleted file mode 100644 index 22b891e..0000000 --- a/HotSpot/Sources/Data/DTO/Response/ShopDTO.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Foundation - -struct ShopDTO: Decodable { - let id: String - let name: String - let address: String - let lat: Double - let lng: Double - let access: String - let open: String? - let photo: Photo - let genre: Genre - let budget: Budget? - let wifi: String? - let nonSmoking: String? - let privateRoom: String? - let parking: String? - let urls: Urls - - struct Photo: Decodable { - let pc: PcPhoto - - struct PcPhoto: Decodable { - let large: String - let medium: String - let small: String - - enum CodingKeys: String, CodingKey { - case large = "l" - case medium = "m" - case small = "s" - } - } - } - - struct Genre: Decodable { - let code: String - let name: String - let catchPhrase: String - - enum CodingKeys: String, CodingKey { - case code - case name - case catchPhrase = "catch" - } - - static func from(code: String) -> HotSpot.Genre? { - return HotSpot.Genre(rawValue: code) - } - } - - struct Budget: Decodable { - let code: String - let name: String - let average: String? - - static func from(code: String) -> HotSpot.Budget? { - return HotSpot.Budget(rawValue: code) - } - } - - struct Urls: Decodable { - let pc: String - } - - enum CodingKeys: String, CodingKey { - case id - case name - case address - case lat - case lng - case access - case open - case photo - case genre - case budget - case wifi - case nonSmoking = "non_smoking" - case privateRoom = "private_room" - case parking - case urls - } - - func toDomain() -> ShopModel { - ShopModel( - id: id, - name: name, - address: address, - latitude: lat, - longitude: lng, - imageUrl: photo.pc.large, - access: access, - openingHours: open ?? "営業時間情報なし", - genre: Genre.from(code: genre.code) ?? .other, - budget: budget.flatMap { Budget.from(code: $0.code) } ?? .from1501to2000, - url: urls.pc, - wifi: wifi == "あり" ? 1 : 0, - privateRoom: privateRoom == "あり" ? 1 : 0, - nonSmoking: nonSmoking == "あり" ? 1 : 0, - parking: parking == "あり" ? 1 : 0 - ) - } -} diff --git a/HotSpot/Sources/Data/DTO/Response/ShopSearchResponseDTO.swift b/HotSpot/Sources/Data/DTO/Response/ShopSearchResponseDTO.swift deleted file mode 100644 index 643d7cc..0000000 --- a/HotSpot/Sources/Data/DTO/Response/ShopSearchResponseDTO.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -struct ShopSearchResponseDTO: Decodable { - let results: ShopSearchResultsDTO - - enum CodingKeys: String, CodingKey { - case results - } -} diff --git a/HotSpot/Sources/Data/DTO/Response/ShopSearchResultsDTO.swift b/HotSpot/Sources/Data/DTO/Response/ShopSearchResultsDTO.swift deleted file mode 100644 index 45ea03d..0000000 --- a/HotSpot/Sources/Data/DTO/Response/ShopSearchResultsDTO.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -struct ShopSearchResultsDTO: Decodable { - let resultsAvailable: Int - let resultsReturned: String - let resultsStart: Int - let shop: [ShopDTO] - - enum CodingKeys: String, CodingKey { - case resultsAvailable = "results_available" - case resultsReturned = "results_returned" - case resultsStart = "results_start" - case shop - } -} diff --git a/HotSpot/Sources/Data/Repository/ShopRepositoryImpl.swift b/HotSpot/Sources/Data/Repository/ShopRepositoryImpl.swift deleted file mode 100644 index 2b95771..0000000 --- a/HotSpot/Sources/Data/Repository/ShopRepositoryImpl.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -final class ShopRepositoryImpl: ShopRepository { - private let remoteDataSource: ShopRemoteDataSource - - init(remoteDataSource: ShopRemoteDataSource) { - self.remoteDataSource = remoteDataSource - } - - func searchShops(request: ShopSearchRequestDTO) async throws -> ShopSearchResponseDTO { - try await remoteDataSource.search(request: request) - } -} diff --git a/HotSpot/Sources/Domain/Model/ShopModel.swift b/HotSpot/Sources/Domain/Model/ShopModel.swift deleted file mode 100644 index da18180..0000000 --- a/HotSpot/Sources/Domain/Model/ShopModel.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation - -struct ShopModel: Identifiable, Equatable { - let id: String - let name: String - let address: String - let latitude: Double - let longitude: Double - let imageUrl: String - let access: String - let openingHours: String - let genre: Genre - let budget: Budget - let url: String - let wifi: Int - let privateRoom: Int - let nonSmoking: Int - let parking: Int - - var coordinate: MapCoordinate { - MapCoordinate(latitude: latitude, longitude: longitude) - } - - static func filterVisibleShops(_ shops: [ShopModel], in region: MapRegion) -> [ShopModel] { - shops.filter { shop in - let latMin = region.center.latitude - region.span.latitudeDelta / 2 - let latMax = region.center.latitude + region.span.latitudeDelta / 2 - let lonMin = region.center.longitude - region.span.longitudeDelta / 2 - let lonMax = region.center.longitude + region.span.longitudeDelta / 2 - - return shop.latitude >= latMin && - shop.latitude <= latMax && - shop.longitude >= lonMin && - shop.longitude <= lonMax - } - } -} diff --git a/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift b/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift deleted file mode 100644 index 498907a..0000000 --- a/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation - -struct InfiniteScrollSearchUseCase { - private let repository: ShopRepository - private let pageSize: Int - - init(repository: ShopRepository, pageSize: Int = 20) { - self.repository = repository - self.pageSize = pageSize - } - - func execute( - request: ShopSearchRequestDTO, - currentPage: Int, - isLoadMore: Bool = false - ) async throws -> SearchResultModel { - let targetPage = isLoadMore ? currentPage + 1 : currentPage - let start = (targetPage - 1) * pageSize + 1 - - let paginatedRequest = ShopSearchRequestDTO( - lat: request.lat, - lng: request.lng, - range: request.range, - count: pageSize, - name: request.name, - genres: request.genres, - start: start, - budgets: request.budgets, - privateRoom: request.privateRoom, - wifi: request.wifi, - nonSmoking: request.nonSmoking, - parking: request.parking, - ) - - let response = try await repository.searchShops(request: paginatedRequest) - return SearchResultModel.from(response: response, currentPage: targetPage) - } -} diff --git a/Project.swift b/Project.swift index ede1553..7e3b82e 100644 --- a/Project.swift +++ b/Project.swift @@ -54,9 +54,13 @@ let project = Project( "NSLocationAlwaysAndWhenInUseUsageDescription": .string("周辺の店舗を表示するために位置情報が必要です。") ] ), - sources: ["\(projectName)/Sources/**"], - resources: ["\(projectName)/Resources/**"], + sources: ["HotSpot/App/Sources/**"], + resources: ["HotSpot/App/Resources/**"], dependencies: [ + .project(target: "Presentation", path: "HotSpot/Presentation"), + .project(target: "Domain", path: "HotSpot/Domain"), + .project(target: "Shared", path: "HotSpot/Shared"), + .project(target: "Data", path: "HotSpot/Data"), .external(name: "CobyDS"), .external(name: "Moya"), .external(name: "ComposableArchitecture"), @@ -69,7 +73,7 @@ let project = Project( product: .unitTests, bundleId: bundleTestID, infoPlist: .default, - sources: ["\(projectName)/Tests/**"], + sources: ["HotSpot/App/Tests/**"], resources: [], dependencies: [.target(name: projectName)] ), @@ -86,4 +90,4 @@ let project = Project( runAction: .runAction(configuration: .release) ) ] -) +) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5cf1512 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# 簡易仕様書 + +### 作者 +Coby + +### アプリ名 +HotSpot + +#### コンセプト +食べに行きたいお店がすぐ見つかる。 + +#### こだわったポイント +- モダンなUI/UXデザイン +- 高速な検索機能 +- 直感的な操作性 + +### 公開したアプリの URL(Store にリリースしている場合) +準備中 + +### 該当プロジェクトのリポジトリ URL +https://github.com/CobyApp/HotSpot + +## 開発環境 +### 開発環境 +Xcode 16.3 + +### 開発言語 +Swift 5.9 + +### テーブル定義(ER図)などの設計ドキュメント + +#### 店舗情報 (Shop) +| カラム名 | 型 | 説明 | 制約 | +|---------|----|------|------| +| id | String | 店舗ID | NOT NULL | +| name | String | 店舗名 | NOT NULL | +| address | String | 住所 | NOT NULL | +| latitude | Double | 緯度 | NOT NULL | +| longitude | Double | 経度 | NOT NULL | +| image_url | String | 画像URL | NOT NULL | +| access | String | アクセス情報 | NOT NULL | +| opening_hours | String | 営業時間 | NOT NULL | +| genre | String | ジャンル | NOT NULL | +| budget | String | 予算 | NOT NULL | +| url | String | 店舗URL | NOT NULL | +| wifi | Int | WiFi有無 | NOT NULL | +| private_room | Int | 個室有無 | NOT NULL | +| non_smoking | Int | 禁煙有無 | NOT NULL | +| parking | Int | 駐車場有無 | NOT NULL | + +#### 検索結果 (SearchResult) +| カラム名 | 型 | 説明 | 制約 | +|---------|----|------|------| +| shops | [Shop] | 店舗リスト | NOT NULL | +| current_page | Int | 現在のページ | NOT NULL | +| has_more | Bool | 次のページ有無 | NOT NULL | +| total_count | Int | 総件数 | NOT NULL | + +### 開発環境構築手順 +1. リポジトリをクローン +```bash +git clone https://github.com/CobyApp/HotSpot +``` + +2. 依存関係のインストール +```bash +tuist install +``` + +3. プロジェクトの生成 +```bash +tuist generate +``` + +## 動作対象端末・OS +### 動作対象OS +iOS 15.0以上 + +## 開発期間 +5日間 + +## アプリケーション機能 + +### 機能一覧 +- レストラン検索:現在地周辺の飲食店を検索する +- レストラン情報取得:飲食店の詳細情報を取得する +- 地図アプリ連携:飲食店の所在地を地図アプリに連携する + +### 画面一覧 +- **地図画面** + - 画面に表示された範囲内の店舗のリストを取得 + - 店舗数が多い場合はクラスタリング適用 + - 店舗マーカーをタップすると、その店舗のカードタイルに焦点が当てられる + +- **詳細画面** + - お店の詳細情報を表示 + - お店のウェブサイトへのリンク + - 地図アプリへの連携機能 + +- **検索画面** + - 店舗名での検索機能 + - ローカルに保存されたフィルター条件の反映 + +- **フィルター画面** + - タグ形式でのフィルター指定 + - リセットボタンによるフィルター内容の初期化 + +### 使用しているAPI,SDK,ライブラリなど +- Composable Architecture (TCA) +- Tuist +- Swift Package Manager +- Moya +- Kingfisher +- Coby DS +- HotPepper API + +### 技術面でアドバイスして欲しいポイント +- パフォーマンス最適化 +- ユーザー体験の向上 +- テストカバレッジの向上 +- アーキテクチャの改善 \ No newline at end of file diff --git a/Workspace.swift b/Workspace.swift new file mode 100644 index 0000000..dd2271a --- /dev/null +++ b/Workspace.swift @@ -0,0 +1,12 @@ +import ProjectDescription + +let workspace = Workspace( + name: "HotSpot", + projects: [ + "HotSpot/App", + "HotSpot/Presentation", + "HotSpot/Domain", + "HotSpot/Shared", + "HotSpot/Data" + ] +) \ No newline at end of file