diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Domain.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Domain.swift index 4b17516..0f5f3df 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Domain.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Domain.swift @@ -12,4 +12,5 @@ public enum Domain: String, Modulable { case AppService case AuthService case PushService + case UserService } diff --git a/Projects/Core/APIClient/Interface/APIClientInterface.swift b/Projects/Core/APIClient/Interface/APIClientInterface.swift index d9fa7cc..c6bbb46 100644 --- a/Projects/Core/APIClient/Interface/APIClientInterface.swift +++ b/Projects/Core/APIClient/Interface/APIClientInterface.swift @@ -22,11 +22,15 @@ public struct APIClient { public func apiRequest( request: APIBaseRequest, - as: T.Type, + as: T.Type = T.self, isWithInterceptor: Bool = true ) async throws -> T { let (data, _) = try await self.apiRequest(request, isWithInterceptor) + if T.self == EmptyResponse.self { + return EmptyResponse() as! T + } + do { let decodedData = try JSONDecoder().decode(T.self, from: data) return decodedData @@ -40,3 +44,6 @@ extension APIClient: TestDependencyKey { public static let previewValue = Self() public static let testValue = Self() } + +// MARK: Empty Response 대응 논의 필요 +public struct EmptyResponse: Decodable {} diff --git a/Projects/Core/APIClient/Sources/TokenInterceptor.swift b/Projects/Core/APIClient/Sources/TokenInterceptor.swift index b45deae..dc46ca5 100644 --- a/Projects/Core/APIClient/Sources/TokenInterceptor.swift +++ b/Projects/Core/APIClient/Sources/TokenInterceptor.swift @@ -12,7 +12,7 @@ import APIClientInterface import Logger import Dependencies -public enum KeychainClientKeys: String { +enum KeychainClientKeys: String { case accessToken = "mohanyang_keychain_access_token" case refreshToken = "mohanyang_keychain_refresh_token" } diff --git a/Projects/Core/KeychainClient/Interface/Keys.swift b/Projects/Core/KeychainClient/Interface/Keys.swift deleted file mode 100644 index 5d00ab7..0000000 --- a/Projects/Core/KeychainClient/Interface/Keys.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Keys.swift -// KeychainClientInterface -// -// Created by 김지현 on 8/9/24. -// Copyright © 2024 PomoNyang. All rights reserved. -// - -import Foundation - -public enum KeychainKeys: String { - case accessToken = "mohanyang_keychain_access_token" - case refreshToken = "mohanyang_keychain_refresh_token" - case deviceID = "mohanyang_keychain_deviceID" -} diff --git a/Projects/Domain/AuthService/Sources/API/AuthAPIClient.swift b/Projects/Domain/AuthService/Sources/API/AuthAPIClient.swift index dafa8fa..2620bec 100644 --- a/Projects/Domain/AuthService/Sources/API/AuthAPIClient.swift +++ b/Projects/Domain/AuthService/Sources/API/AuthAPIClient.swift @@ -12,25 +12,34 @@ import UserDefaultsClientInterface import AuthServiceInterface import Dependencies +enum KeychainClientKeys: String { + case accessToken = "mohanyang_keychain_access_token" + case refreshToken = "mohanyang_keychain_refresh_token" +} + extension AuthService: DependencyKey { public static let liveValue: AuthService = .live() private static func live() -> Self { return AuthService( login: { deviceID, apiClient, keychainClient in - guard isTokenValid(keychainClient) else { return } + guard !isTokenValid(keychainClient) else { return } + let service = AuthAPIRequest.login(deviceID) let response = try await apiClient.apiRequest( request: service, as: AuthDTO.Response.TokenResponseDTO.self, isWithInterceptor: false ) - _ = keychainClient.create(key: KeychainKeys.accessToken.rawValue, data: response.accessToken) + + _ = keychainClient.create(key: KeychainClientKeys.accessToken.rawValue, data: response.accessToken) + _ = keychainClient.create(key: KeychainClientKeys.refreshToken.rawValue, data: response.refreshToken) return } ) } private static func isTokenValid(_ keychainClient: KeychainClient) -> Bool { - return keychainClient.read(key: KeychainKeys.accessToken.rawValue) != nil + let isTokenExist = keychainClient.read(key: KeychainClientKeys.accessToken.rawValue) + return isTokenExist != nil } } diff --git a/Projects/Domain/Domain/Sources/Exports.swift b/Projects/Domain/Domain/Sources/Exports.swift index 9059185..c92564b 100644 --- a/Projects/Domain/Domain/Sources/Exports.swift +++ b/Projects/Domain/Domain/Sources/Exports.swift @@ -10,3 +10,5 @@ @_exported import PushService @_exported import AuthService @_exported import AuthServiceInterface +@_exported import UserService +@_exported import UserServiceInterface diff --git a/Projects/Domain/UserService/Interface/API/UserAPIClientInterface.swift b/Projects/Domain/UserService/Interface/API/UserAPIClientInterface.swift new file mode 100644 index 0000000..b65637d --- /dev/null +++ b/Projects/Domain/UserService/Interface/API/UserAPIClientInterface.swift @@ -0,0 +1,31 @@ +// +// UserAPIClientInterface.swift +// UserService +// +// Created by 김지현 on 8/16/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import Foundation + +import APIClientInterface + +import Dependencies +import DependenciesMacros + + +@DependencyClient +public struct UserService { + public var fetchCatLists: @Sendable ( + _ apiClient: APIClient + ) async throws -> CatList + public var selectCat: @Sendable ( + _ no: Int, + _ apiClient: APIClient + ) async throws -> Void +} + +extension UserService: TestDependencyKey { + public static let previewValue = Self() + public static let testValue = Self() +} diff --git a/Projects/Domain/UserService/Interface/API/UserAPIRequest.swift b/Projects/Domain/UserService/Interface/API/UserAPIRequest.swift new file mode 100644 index 0000000..babbc87 --- /dev/null +++ b/Projects/Domain/UserService/Interface/API/UserAPIRequest.swift @@ -0,0 +1,48 @@ +// +// UserAPIRequest.swift +// UserService +// +// Created by 김지현 on 8/16/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import Foundation +import APIClientInterface + +public enum UserAPIrequest { + case fetchCatList, selectCat(Int) +} + +extension UserAPIrequest: APIBaseRequest { + public var baseURL: String { + return API.apiBaseURL + } + + public var path: String { + switch self { + case .fetchCatList: + return "/api/v1/cats" + case .selectCat: + return "/api/v1/users/cats" + } + } + + public var method: HTTPMethod { + switch self { + case .fetchCatList: + return .get + case .selectCat: + return .put + } + } + + public var parameters: RequestParams { + switch self { + case .fetchCatList: + return .requestPlain + case .selectCat(let no): + let dto = UserDTO.Request.SelectCatRequestDTO(catNo: no) + return .body(dto) + } + } +} diff --git a/Projects/Domain/UserService/Interface/DTO/UserDTO.swift b/Projects/Domain/UserService/Interface/DTO/UserDTO.swift new file mode 100644 index 0000000..24f86b2 --- /dev/null +++ b/Projects/Domain/UserService/Interface/DTO/UserDTO.swift @@ -0,0 +1,32 @@ +// +// UserDTO.swift +// UserService +// +// Created by 김지현 on 8/16/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import Foundation + +public typealias CatList = [UserDTO.Response.GetCatListResponseDTO] + +public enum UserDTO { + public enum Request { } + public enum Response { } +} + +public extension UserDTO.Request { + struct SelectCatRequestDTO: Encodable { + public var catNo: Int + } +} + +public extension UserDTO.Response { + struct GetCatListResponseDTO: Equatable, Decodable { + public var no: Int + public var name: String + public var type: String + } + + struct SelectCatResponseDTO: Decodable { } +} diff --git a/Projects/Domain/UserService/Project.swift b/Projects/Domain/UserService/Project.swift new file mode 100644 index 0000000..259b532 --- /dev/null +++ b/Projects/Domain/UserService/Project.swift @@ -0,0 +1,20 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +@_spi(Domain) +@_spi(Core) +import DependencyPlugin + +let project: Project = .makeTMABasedProject( + module: Domain.UserService, + scripts: [], + targets: [ + .sources, + .interface + ], + dependencies: [ + .interface: [ + .dependency(rootModule: Core.self) + ] + ] +) diff --git a/Projects/Domain/UserService/Sources/API/UserAPIClient.swift b/Projects/Domain/UserService/Sources/API/UserAPIClient.swift new file mode 100644 index 0000000..91077c3 --- /dev/null +++ b/Projects/Domain/UserService/Sources/API/UserAPIClient.swift @@ -0,0 +1,37 @@ +// +// UserAPIClient.swift +// UserService +// +// Created by 김지현 on 8/16/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import Foundation + +import APIClientInterface +import UserServiceInterface + +import Dependencies + +extension UserService: DependencyKey { + public static let liveValue: UserService = .live() + private static func live() -> Self { + return UserService( + fetchCatLists: { apiClient in + let request = UserAPIrequest.fetchCatList + return try await apiClient.apiRequest( + request: request, + as: CatList.self + ) + }, + selectCat: { no, apiClient in + let request = UserAPIrequest.selectCat(no) + _ = try await apiClient.apiRequest( + request: request, + as: EmptyResponse.self + ) + return + } + ) + } +} diff --git a/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingCore.swift b/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingCore.swift index d71336e..eb1f105 100644 --- a/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingCore.swift +++ b/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingCore.swift @@ -20,18 +20,21 @@ public struct OnboardingCore { var width: CGFloat = .zero var currentIdx: Int = 0 var currentItemID: String = "" + @Presents var selectCat: SelectCatCore.State? } public enum Action: BindableAction { case onApear case calculateOffset(CGFloat, OnboardingItem) case dragStart + case tapStartButton case _timerStart case _timerEnd case _timerTicked case _nextPage(Int) case _resetTofront case binding(BindingAction) + case selectCat(PresentationAction) } public init() { } @@ -42,6 +45,9 @@ public struct OnboardingCore { public var body: some ReducerOf { BindingReducer() Reduce(self.core) + .ifLet(\.$selectCat, action: \.selectCat) { + SelectCatCore() + } } private func core(_ state: inout State, _ action: Action) -> EffectOf { @@ -90,6 +96,10 @@ public struct OnboardingCore { timerStartAction ) + case .tapStartButton: + state.selectCat = SelectCatCore.State() + return .run { send in await send(._timerEnd) } + case ._timerStart: return .run { send in for await _ in self.clock.timer(interval: .seconds(3)) { @@ -122,7 +132,10 @@ public struct OnboardingCore { state.currentItemID = state.fakedData[1].id.uuidString return .none - default: + case .binding: + return .none + + case .selectCat: return .none } } diff --git a/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingView.swift b/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingView.swift index 65e52b6..bb3c87b 100644 --- a/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingView.swift +++ b/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingView.swift @@ -19,9 +19,7 @@ public struct OnboardingView: View { } public var body: some View { - ZStack { - Alias.Color.Background.primary - .ignoresSafeArea() + NavigationStack { VStack { Spacer() VStack(spacing: 0) { @@ -54,15 +52,24 @@ public struct OnboardingView: View { .padding(.vertical, 32) Button(title: "시작하기") { - // 다음 뷰 이동 + store.send(.tapStartButton) } .buttonStyle(.box(level: .primary, size: .large, width: .medium)) .padding(.top, 16) } Spacer() } + .background { + Alias.Color.Background.primary + .ignoresSafeArea() + } + .onAppear { store.send(.onApear) } + .navigationDestination( + item: $store.scope(state: \.selectCat, action: \.selectCat) + ) { store in + SelectCatView(store: store) + } } - .onAppear { store.send(.onApear) } .background { GeometryReader { geometry in Color.clear diff --git a/Projects/Feature/OnboardingFeature/Sources/SelectCat/CatFactory.swift b/Projects/Feature/OnboardingFeature/Sources/SelectCat/CatFactory.swift new file mode 100644 index 0000000..a2a4f33 --- /dev/null +++ b/Projects/Feature/OnboardingFeature/Sources/SelectCat/CatFactory.swift @@ -0,0 +1,98 @@ +// +// CatFactory.swift +// OnboardingFeature +// +// Created by 김지현 on 8/15/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import SwiftUI +import DesignSystem + +public enum CatType: String, Equatable { + case cheese, black, threeColor +} + +// MARK: ANY CAT +public struct AnyCat: CatFactoryProtocol, Identifiable, Equatable { + private let base: CatFactoryProtocol + public init(_ base: CatFactoryProtocol) { + self.base = base + } + + public var id: String { base.id } + public var no: Int { base.no } + public var keyword: String { base.keyword } + public var keywordImage: Image { base.keywordImage } + public var name: String { base.name } + public var catImage: Image { base.catImage } + public var pushNotificationTitle: String { base.pushNotificationTitle } + + public static func == (lhs: AnyCat, rhs: AnyCat) -> Bool { + lhs.base.id == rhs.base.id + } +} + +// MARK: CHEESE CAT +public struct CheeseCat: CatFactoryProtocol { + public init(no: Int, name: String) { + self.no = no + self.name = name + } + + public var id: String = "CHEESE" + public var no: Int + public var name: String + public var keyword: String = "응원" + public var keywordImage: Image = DesignSystemAsset.Image._16Star.swiftUIImage + public var catImage: Image = Image(systemName: "star.fill") + public var pushNotificationTitle: String = "어디갔냐옹..." +} + +// MARK: BLACK CAT +public struct BlackCat: CatFactoryProtocol { + public init(no: Int, name: String) { + self.no = no + self.name = name + } + + public var id: String = "BLACK" + public var no: Int + public var name: String + public var keyword: String = "긍정" + public var keywordImage: Image = DesignSystemAsset.Image._16Heart.swiftUIImage + public var catImage: Image = Image(systemName: "star") + public var pushNotificationTitle: String = "어디갔냐옹..." +} + +// MARK: THREE_COLOR CAT +public struct ThreeColorCat: CatFactoryProtocol { + public init(no: Int, name: String) { + self.no = no + self.name = name + } + + public var id: String = "THREE_COLOR" + public var no: Int + public var name: String + public var keyword: String = "자극" + public var keywordImage: Image = DesignSystemAsset.Image._16Focus.swiftUIImage + public var catImage: Image = Image(systemName: "star.fill") + public var pushNotificationTitle: String = "내가 여기있는데 어디갔냐옹!" +} + +// MARK: MAKE CAT +public struct CatFactory { + public static func makeCat(type: CatType, no: Int, name: String) -> AnyCat { + let cat: CatFactoryProtocol + switch type { + case .cheese: + cat = CheeseCat(no: no, name: name) + case .black: + cat = BlackCat(no: no, name: name) + case .threeColor: + cat = ThreeColorCat(no: no, name: name) + } + return AnyCat(cat) + } +} diff --git a/Projects/Feature/OnboardingFeature/Sources/SelectCat/CatFactoryProtocol.swift b/Projects/Feature/OnboardingFeature/Sources/SelectCat/CatFactoryProtocol.swift new file mode 100644 index 0000000..a63f89f --- /dev/null +++ b/Projects/Feature/OnboardingFeature/Sources/SelectCat/CatFactoryProtocol.swift @@ -0,0 +1,19 @@ +// +// CatFactoryProtocol.swift +// OnboardingFeature +// +// Created by 김지현 on 8/16/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import SwiftUI + +public protocol CatFactoryProtocol { + var id: String { get } // catType의 rawValue + var no: Int { get } // 서버에서 주는 no + var name: String { get } // 서버에서 주는 이름 + var keyword: String { get } + var keywordImage: Image { get } // 키워드에 따른 아이콘 이미지 + var catImage: Image { get } + var pushNotificationTitle: String { get } // 푸시알림 예시 글귀 +} diff --git a/Projects/Feature/OnboardingFeature/Sources/SelectCat/SelectCatCore.swift b/Projects/Feature/OnboardingFeature/Sources/SelectCat/SelectCatCore.swift new file mode 100644 index 0000000..e165417 --- /dev/null +++ b/Projects/Feature/OnboardingFeature/Sources/SelectCat/SelectCatCore.swift @@ -0,0 +1,87 @@ +// +// SelectCatCore.swift +// OnboardingFeature +// +// Created by 김지현 on 8/14/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import APIClientInterface +import UserServiceInterface +import UserNotificationClientInterface +import Shared + +import ComposableArchitecture + +@Reducer +public struct SelectCatCore { + @ObservableState + public struct State: Equatable { + var catList: [AnyCat] = [] + //var catType: CatType? = nil + var selectedCat: AnyCat? = nil + } + + public enum Action: BindableAction { + case onAppear + case selectCat(AnyCat) + case tapNextButton + case _fetchCatListRequest + case _fetchCatListResponse(CatList) + case _selectCatRequest + case binding(BindingAction) + } + + public init() {} + + @Dependency(APIClient.self) var apiClient + @Dependency(UserService.self) var userService + @Dependency(UserNotificationClient.self) var userNotificationClient + + public var body: some ReducerOf { + BindingReducer() + Reduce(self.core) + } + + private func core(state: inout State, action: Action) -> EffectOf { + switch action { + case .onAppear: + return .run { send in await send(._fetchCatListRequest) } + + case.selectCat(let selectedCat): + state.selectedCat = (state.selectedCat == selectedCat) ? nil : selectedCat + return .none + + case .tapNextButton: + return .run { send in await send(._selectCatRequest) } + + case ._fetchCatListRequest: + return .run { send in + let response = try await userService.fetchCatLists(apiClient: apiClient) + await send(._fetchCatListResponse(response)) + } + + case ._fetchCatListResponse(let catList): + state.catList = catList.map { cat in + CatFactory.makeCat( + type: CatType(rawValue: cat.type.camelCased()) ?? .cheese, + no: cat.no, + name: cat.name + ) + } + return .none + + case ._selectCatRequest: + guard let selectedCat = state.selectedCat else { return .none } + return .run { send in + _ = try await userService.selectCat(no: selectedCat.no, apiClient: apiClient) + // user notification 요청 + _ = try await userNotificationClient.requestAuthorization([.alert, .badge, .sound]) + // go to naming cat + } + + case .binding: + return .none + } + } +} diff --git a/Projects/Feature/OnboardingFeature/Sources/SelectCat/SelectCatView.swift b/Projects/Feature/OnboardingFeature/Sources/SelectCat/SelectCatView.swift new file mode 100644 index 0000000..5ce48e1 --- /dev/null +++ b/Projects/Feature/OnboardingFeature/Sources/SelectCat/SelectCatView.swift @@ -0,0 +1,125 @@ +// +// SelectCatView.swift +// OnboardingFeature +// +// Created by 김지현 on 8/14/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import SwiftUI + +import DesignSystem + +import ComposableArchitecture + +public struct SelectCatView: View { + @Bindable var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + NavigationContainer( + title: Text("고양이 선택"), + style: .navigation + ) { + VStack(spacing: 0) { + HStack { + VStack(spacing: Alias.Spacing.xSmall) { + Text("어떤 고양이와 함께할까요?") + .font(Typography.header3) + .foregroundStyle(Alias.Color.Text.primary) + Text("언제든지 다른 고양이를 선택할 수 있어요") + .font(Typography.bodyR) + .foregroundStyle(Alias.Color.Text.secondary) + } + Spacer() + } + .padding(.top, Alias.Spacing.xLarge) + + Spacer(minLength: Alias.Spacing.xLarge) + + VStack(spacing: Alias.Spacing.small) { + CatPushNotificationExampleView(selectedCat: $store.selectedCat) + ZStack { + Rectangle() + .foregroundStyle(Alias.Color.Background.secondary) + .frame(height: 240) + store.selectedCat?.catImage + } + + HStack { + ForEach(store.catList) { cat in + Button( + title: LocalizedStringKey(cat.name), + subtitle: LocalizedStringKey(cat.keyword), + rightIcon: cat.keywordImage, + action: { store.send(.selectCat(cat)) } + ) + .buttonStyle(.select(isSelected: cat == store.selectedCat)) + } + } + .padding(.top, 34) + } + + Spacer(minLength: Alias.Spacing.large) + + Button(title: "이 고양이와 함께하기") { + store.send(.tapNextButton) + } + .buttonStyle(.box(level: .primary, size: .large, width: .low)) + .disabled(store.selectedCat == nil) + } + .padding(.horizontal, 20) + } + .onAppear { store.send(.onAppear) } + .background { + Alias.Color.Background.primary + .ignoresSafeArea() + } + } +} + +struct CatPushNotificationExampleView: View { + @Binding var selectedCat: AnyCat? + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: Alias.BorderRadius.xSmall,style: .circular) + .foregroundStyle(Alias.Color.Background.secondary) + + if let selectedCat = selectedCat { + HStack(spacing: 10){ + Image(systemName: "star.fill") + VStack(spacing: 0) { + HStack { + Text("모하냥") + .font(Typography.bodyR) + .foregroundStyle(Alias.Color.Text.primary) + Spacer() + Text("지금") + .font(Typography.subBodyR) + .foregroundStyle(Alias.Color.Text.secondary) + } + HStack { + Text(selectedCat.pushNotificationTitle) + .font(Typography.subBodyR) + .foregroundStyle(Alias.Color.Text.primary) + Spacer() + } + } + Spacer() + } + .padding(.all, Alias.Spacing.medium) + } else { + Text("고양이를 선택하면\n딴 짓 방해알림 예시를 보여드려요") + .font(Typography.bodyR) + .foregroundStyle(Alias.Color.Text.tertiary) + .multilineTextAlignment(.center) + .padding(.all, Alias.Spacing.medium) + } + } + .frame(height: 72) + } +} diff --git a/Projects/Feature/SplashFeature/Sources/SplashCore.swift b/Projects/Feature/SplashFeature/Sources/SplashCore.swift index 48a023b..39b01e5 100644 --- a/Projects/Feature/SplashFeature/Sources/SplashCore.swift +++ b/Projects/Feature/SplashFeature/Sources/SplashCore.swift @@ -33,6 +33,8 @@ public struct SplashCore { public init() { } + let deviceIDKey = "mohanyang_keychain_device_id" + @Dependency(APIClient.self) var apiClient @Dependency(AuthService.self) var authService @Dependency(DatabaseClient.self) var databaseClient @@ -60,7 +62,7 @@ public struct SplashCore { extension SplashCore { private func checkDeviceIDExist() -> Effect { - let deviceID = keychainClient.read(key: KeychainKeys.deviceID.rawValue) ?? getDeviceUUID() + let deviceID = keychainClient.read(key: deviceIDKey) ?? getDeviceUUID() return login(deviceID: deviceID) } @@ -81,7 +83,7 @@ extension SplashCore { private func getDeviceUUID() -> String { guard let uuid = UIDevice.current.identifierForVendor?.uuidString, - keychainClient.create(key: KeychainKeys.deviceID.rawValue, data: uuid) else { + keychainClient.create(key: "mohanyang_keychain_device_id", data: uuid) else { return "" } return uuid diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_focus.imageset/16_focus.svg b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_focus.imageset/16_focus.svg new file mode 100644 index 0000000..66d94b3 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_focus.imageset/16_focus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_focus.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_focus.imageset/Contents.json new file mode 100644 index 0000000..5eafda9 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_focus.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "16_focus.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_heart.imageset/16_heart.svg b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_heart.imageset/16_heart.svg new file mode 100644 index 0000000..9ed635e --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_heart.imageset/16_heart.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_heart.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_heart.imageset/Contents.json new file mode 100644 index 0000000..f76510e --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_heart.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "16_heart.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_star.imageset/16_star.svg b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_star.imageset/16_star.svg new file mode 100644 index 0000000..d79a658 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_star.imageset/16_star.svg @@ -0,0 +1,3 @@ + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_star.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_star.imageset/Contents.json new file mode 100644 index 0000000..f59498c --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/16_star.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "16_star.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/Contents.json b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image.xcassets/16/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/24_arrow_left_primary.imageset/24_arrow_left_primary.svg b/Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_arrow_left_primary.imageset/24_arrow_left_primary.svg similarity index 100% rename from Projects/Shared/DesignSystem/Resources/Image.xcassets/24_arrow_left_primary.imageset/24_arrow_left_primary.svg rename to Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_arrow_left_primary.imageset/24_arrow_left_primary.svg diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/24_arrow_left_primary.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_arrow_left_primary.imageset/Contents.json similarity index 100% rename from Projects/Shared/DesignSystem/Resources/Image.xcassets/24_arrow_left_primary.imageset/Contents.json rename to Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_arrow_left_primary.imageset/Contents.json diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/24_cancel_primary.imageset/24_cancel_primary.svg b/Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_cancel_primary.imageset/24_cancel_primary.svg similarity index 100% rename from Projects/Shared/DesignSystem/Resources/Image.xcassets/24_cancel_primary.imageset/24_cancel_primary.svg rename to Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_cancel_primary.imageset/24_cancel_primary.svg diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/24_cancel_primary.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_cancel_primary.imageset/Contents.json similarity index 100% rename from Projects/Shared/DesignSystem/Resources/Image.xcassets/24_cancel_primary.imageset/Contents.json rename to Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_cancel_primary.imageset/Contents.json diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/24/Contents.json b/Projects/Shared/DesignSystem/Resources/Image.xcassets/24/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image.xcassets/24/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Shared/Utils/Sources/Extensions/String+Extension.swift b/Projects/Shared/Utils/Sources/Extensions/String+Extension.swift new file mode 100644 index 0000000..9bdffa2 --- /dev/null +++ b/Projects/Shared/Utils/Sources/Extensions/String+Extension.swift @@ -0,0 +1,22 @@ +// +// String+.swift +// Utils +// +// Created by 김지현 on 8/16/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import Foundation + +extension String { + public func camelCased() -> String { + let components = self.lowercased().split(separator: "_") + let camelCased = components + .enumerated() + .map { index, component in + index == 0 ? String(component) : component.capitalized + } + .joined() + return camelCased + } +}