diff --git a/Projects/Core/DesignSystem/Sources/MainTab/MainTab.swift b/Projects/Core/DesignSystem/Sources/MainTab/MainTab.swift index ea92381..63990af 100644 --- a/Projects/Core/DesignSystem/Sources/MainTab/MainTab.swift +++ b/Projects/Core/DesignSystem/Sources/MainTab/MainTab.swift @@ -53,7 +53,7 @@ public enum MainTab: String, CaseIterable, Identifiable { public var title: String { switch self { case .recommend: - return "추천" + return "둘러보기" case .search: return "검색" case .mypage: diff --git a/Projects/Core/DesignSystem/Sources/View/CircleButton.swift b/Projects/Core/DesignSystem/Sources/View/CircleButton.swift index 005b100..6913151 100644 --- a/Projects/Core/DesignSystem/Sources/View/CircleButton.swift +++ b/Projects/Core/DesignSystem/Sources/View/CircleButton.swift @@ -38,12 +38,14 @@ public struct CircleButton: View { .overlay { self.image .resizable() + .animation(nil) .scaledToFill() .frame(width: 20, height: 20, alignment: .center) } .frame(width: self.width, height: self.height,alignment: .center) .shadow(color: shadowColor, radius: 20) } + .buttonStyle(PlainButtonStyle()) } } diff --git a/Projects/Core/PPACModels/Sources/User/CreateUserRequestDTO.swift b/Projects/Core/PPACData/Sources/DTO/CreateUserRequestDTO.swift similarity index 100% rename from Projects/Core/PPACModels/Sources/User/CreateUserRequestDTO.swift rename to Projects/Core/PPACData/Sources/DTO/CreateUserRequestDTO.swift diff --git a/Projects/Core/PPACData/Sources/DTO/MemeReactionRequestDTO.swift b/Projects/Core/PPACData/Sources/DTO/MemeReactionRequestDTO.swift index 28ceaef..2701b4d 100644 --- a/Projects/Core/PPACData/Sources/DTO/MemeReactionRequestDTO.swift +++ b/Projects/Core/PPACData/Sources/DTO/MemeReactionRequestDTO.swift @@ -2,20 +2,12 @@ // MemeReactionRequestDTO.swift // PPACData // -// Created by kimchansoo on 10/1/24. +// Created by 김종윤 on 9/28/24. // import Foundation -public struct MemeReactionRequestDTO: Codable { - public let count: Int - - public init(count: Int) { - self.count = count - } -} - -public struct MemeReactionResponseDTO: Codable { +struct MemeReactionRequestDTO: Encodable { public let count: Int public init(count: Int) { diff --git a/Projects/Core/PPACData/Sources/DTO/MemeReactionResponseDTO.swift b/Projects/Core/PPACData/Sources/DTO/MemeReactionResponseDTO.swift new file mode 100644 index 0000000..1f7545e --- /dev/null +++ b/Projects/Core/PPACData/Sources/DTO/MemeReactionResponseDTO.swift @@ -0,0 +1,16 @@ +// +// MemeReactionResponseDTO.swift +// PPACData +// +// Created by 김종윤 on 9/28/24. +// + +import Foundation + +struct MemeReactionResponseDTO: Decodable { + public let count: Int + + public init(count: Int) { + self.count = count + } +} diff --git a/Projects/Core/PPACData/Sources/Endpoint/MemeEndpoint.swift b/Projects/Core/PPACData/Sources/Endpoint/MemeEndpoint.swift index a39dfe5..cd19117 100644 --- a/Projects/Core/PPACData/Sources/Endpoint/MemeEndpoint.swift +++ b/Projects/Core/PPACData/Sources/Endpoint/MemeEndpoint.swift @@ -62,7 +62,7 @@ public enum MemeEndpoint: Requestable { case .watch(let memeId, let type): return "/meme/\(memeId)/watch/\(type)" case .reaction(let memeId, _): - return "meme/\(memeId)/reaction" + return "/meme/\(memeId)/reaction" } } @@ -85,7 +85,7 @@ public enum MemeEndpoint: Requestable { return nil case .watch: return nil - case let .reaction(memeId, count): + case .reaction(_, let count): return .body(MemeReactionRequestDTO(count: count)) case .meme(memeId: _): return nil diff --git a/Projects/Core/PPACDomain/Sources/Repository/MemeRepository.swift b/Projects/Core/PPACDomain/Sources/Repository/MemeRepository.swift index a8b4eed..ed25652 100644 --- a/Projects/Core/PPACDomain/Sources/Repository/MemeRepository.swift +++ b/Projects/Core/PPACDomain/Sources/Repository/MemeRepository.swift @@ -20,6 +20,5 @@ public protocol MemeRepository { func shareMeme(memeId: String) async throws func watchMeme(memeId: String, type: String) async throws func reactToMeme(memeId: String, count: Int) async throws -> Int - func registerMeme(formData: FormData, title: String, source: String, keywordIds: [String]) async throws } diff --git a/Projects/Core/PPACDomain/Sources/UseCase/Meme/ReactToMemeUseCase.swift b/Projects/Core/PPACDomain/Sources/UseCase/Meme/ReactToMemeUseCase.swift index 9208dff..4667b30 100644 --- a/Projects/Core/PPACDomain/Sources/UseCase/Meme/ReactToMemeUseCase.swift +++ b/Projects/Core/PPACDomain/Sources/UseCase/Meme/ReactToMemeUseCase.swift @@ -14,13 +14,13 @@ public protocol ReactToMemeUseCase { } public class ReactToMemeUseCaseImpl: ReactToMemeUseCase { - private let repository: MemeRepository - - public init(repository: MemeRepository) { - self.repository = repository - } - - public func execute(memeId: String, count: Int) async throws -> Int { - try await repository.reactToMeme(memeId: memeId, count: count) - } + private let repository: MemeRepository + + public init(repository: MemeRepository) { + self.repository = repository + } + + public func execute(memeId: String, count: Int) async throws -> Int { + return try await repository.reactToMeme(memeId: memeId, count: count) + } } diff --git a/Projects/Core/PPACNetwork/Sources/Requestable.swift b/Projects/Core/PPACNetwork/Sources/Requestable.swift index d1c4f5c..af8acbc 100644 --- a/Projects/Core/PPACNetwork/Sources/Requestable.swift +++ b/Projects/Core/PPACNetwork/Sources/Requestable.swift @@ -33,7 +33,7 @@ public protocol Requestable { extension Requestable { private var baseUrl: String { - return "http://ppac-server.run.goorm.io/api/" // 개발 서버 + return "http://ppac-server.run.goorm.io/api" // 개발 서버 //return "https://ppac-server-goorm.run.goorm.site/api" // 운영 서버 } diff --git a/Projects/Core/PPACUtil/Sources/Throttler.swift b/Projects/Core/PPACUtil/Sources/Throttler.swift new file mode 100644 index 0000000..4506e18 --- /dev/null +++ b/Projects/Core/PPACUtil/Sources/Throttler.swift @@ -0,0 +1,38 @@ +// +// Throttler.swift +// PPACUtil +// +// Created by 김종윤 on 9/25/24. +// + +import Foundation + +public class Throttler { + private var workItem: DispatchWorkItem? + private var lastExecution: Date? + private let queue: DispatchQueue + private let interval: TimeInterval + + public init(seconds: TimeInterval, queue: DispatchQueue = DispatchQueue.main) { + self.interval = seconds + self.queue = queue + } + + public func throttle(action: @escaping () -> Void) { + // 기존의 예약된 작업이 있으면 취소 + workItem?.cancel() + + // 새로운 작업 생성 + workItem = DispatchWorkItem { [weak self] in + self?.lastExecution = Date() + action() + } + + // 인터벌 후에 작업을 실행하도록 예약 (즉시 실행하지 않음) + queue.asyncAfter(deadline: .now() + interval, execute: workItem!) + } + + public func cancel() { + workItem?.cancel() + } +} diff --git a/Projects/DemoApp/Sources/DemoApp.swift b/Projects/DemoApp/Sources/DemoApp.swift index 6cae271..8aabc9d 100644 --- a/Projects/DemoApp/Sources/DemoApp.swift +++ b/Projects/DemoApp/Sources/DemoApp.swift @@ -71,7 +71,11 @@ private extension DemoApp.Views { router: nil, searchKeywordUseCase: SearchKeywordUseCaseImpl( repository: MemeRepositoryImpl(networkservice: NetworkService()) - ), copyImageUseCase: CopyImageUseCaseImpl() + ), + copyImageUseCase: CopyImageUseCaseImpl(), + watchMemeUseCase: WatchMemeUseCaseImpl( + repository: MemeRepositoryImpl(networkservice: NetworkService()) + ) ) ) } diff --git a/Projects/Features/Recommend/Sources/Presentation/RecommendHeaderView.swift b/Projects/Features/Recommend/Sources/Presentation/RecommendHeaderView.swift index 0c40c17..393d7ac 100644 --- a/Projects/Features/Recommend/Sources/Presentation/RecommendHeaderView.swift +++ b/Projects/Features/Recommend/Sources/Presentation/RecommendHeaderView.swift @@ -10,101 +10,77 @@ import SkeletonUI import ResourceKit +import Lottie + struct RecommendHeaderView: View { - @Binding var userLevel: Int - @Binding var seenMemeCount: Int - @Binding var recommendMemeSize: Int + let isLoad: Bool + let uploadButtonTap: () -> Void public var body: some View { - let isNotLoading = userLevel == 0 || seenMemeCount == 0 VStack(spacing: 0) { ResourceKitAsset.Icon.homeLogo.swiftUIImage .resizable() - .frame(width: 212, height: 45, alignment: .center) - .padding(.bottom, 10) + .frame(width: 106, height: 32, alignment: .center) + .padding(.bottom, 12) recommendTitle - if !isNotLoading { - recommendProgressBar( - seenMemeCount: self.seenMemeCount, - total: recommendMemeSize - ) - - recommendText(getRecommendText()) + if isLoad { + memeUploadButton(uploadButtonTap) + .padding(.top, 20) + .padding(.bottom, 28) } else { EmptyView() - .recommendSkeleton(isShow: isNotLoading, radius: 4, width: 200, height: 16) + .recommendSkeleton(isShow: !isLoad, radius: 4, width: 130, height: 36) .padding(.top, 20) - .padding(.bottom, 37) + .padding(.bottom, 28) } } } - - private func getRecommendText() -> String { - return if userLevel == 0 || seenMemeCount == 0 { - "확인한 밈을 불러오지 못 했어요. 새로고침 해주세요" - } else if userLevel == 1 && - (1...(recommendMemeSize - 1)).contains(seenMemeCount) - { - "밈 보고 레벨 포인트 받아요!" - } else if userLevel == 2 && - (1...(recommendMemeSize - 1)).contains(seenMemeCount) - { - "추천 밈 둘러보세요!" - } else { - "완밈! 다음 주 밈도 기대해 주세요" - } - } } private var recommendTitle : some View { - Text("이번 주 이 밈 어때!") - .font(Font.Heading.Large.semiBold) - .padding(.bottom, 16) -} - -private func recommendProgressBar( - seenMemeCount: Int, - total: Int -) -> some View { - HStack(spacing: 0) { - ResourceKitAsset.Icon.squareCheck.swiftUIImage + VStack(spacing: 0) { + Text("NEW! 따끈따끈한 밈") + .font(Font.Heading.Large.semiBold) + .padding(.bottom, 4) - ProgressView( - value: Double(seenMemeCount), - total: Double(total) - ) - .frame(width: 125, height: 8) - .tint(Color.Icon.brand) - .padding(.vertical, 4) - .padding(.horizontal, 8) - - Text("\(seenMemeCount == 0 ? "?" : "\(seenMemeCount)")개 봤어요") - .font(Font.Body.Small.semiBold) - .foregroundColor(Color.Text.brand) + Text("최근에 사람들이 올린 밈 구경하세요.") + .font(Font.Body.Medium.medium) + .foregroundStyle(Color.Text.secondary) } } -private func recommendText(_ text: String) -> some View { - Text(text) - .font(Font.Body.Medium.medium) - .foregroundStyle(Color.Text.secondary) - .padding(.top, 8) - .padding(.bottom, 32) +private func memeUploadButton( + _ uploadButtonTap: @escaping () -> Void +) -> some View { + @State var playbackMode: LottiePlaybackMode = .paused(at: .progress(100)) + + return Button( + action: { + uploadButtonTap() + }, + label: { + LottieView( + animation: AnimationAsset.uploadLottie.animation + ) + .looping() + } + ) + .buttonStyle(PlainButtonStyle()) + .frame(width: 130, height: 36) + .contentShape(RoundedRectangle(cornerRadius: 10)) } + #Preview { - @State var userLevel: Int = 0 - @State var seenMemeCount: Int = 5 - @State var recommendMemeSize: Int = 5 - return RecommendHeaderView( - userLevel: $userLevel, - seenMemeCount: $seenMemeCount, - recommendMemeSize: $recommendMemeSize + isLoad: true, + uploadButtonTap: { + print("Upload Button Tap!") + } ) .frame(maxWidth: .infinity, maxHeight: .infinity) .background( diff --git a/Projects/Features/Recommend/Sources/Presentation/RecommendMemeButtonView.swift b/Projects/Features/Recommend/Sources/Presentation/RecommendMemeButtonView.swift index db6b04a..a84db27 100644 --- a/Projects/Features/Recommend/Sources/Presentation/RecommendMemeButtonView.swift +++ b/Projects/Features/Recommend/Sources/Presentation/RecommendMemeButtonView.swift @@ -11,18 +11,20 @@ import ResourceKit import DesignSystem import PPACModels +import PPACUtil import Lottie struct RecommendMemeButtonView : View { @State var playbackMode: LottiePlaybackMode = .paused - @State var isTapLikeButton: Bool = false + @State var likeTapCnt: Int = 0 + @State var likeTapMemeId: String? = nil + @State var throttler = Throttler(seconds: 3.0) + + @Binding var meme: MemeDetail? - var isReaction: Bool - var reactionCnt: Int - var isFarmemed: Bool let isOverlapView: Bool - let reactionButtonTapped: () -> Void + let reactionButtonTapped: (String?, Int) -> Void let copyButtonTapped: () -> Void let shareButtonTapped: () -> Void let saveButtonTapped: () -> Void @@ -30,22 +32,28 @@ struct RecommendMemeButtonView : View { public var body: some View { HStack { LikeButton( - isReaction: isReaction, - reactionCount: reactionCnt, + isReaction: meme?.isReaction, + reactionCount: meme?.reaction, didTapped: { - self.isTapLikeButton = true playbackMode = .playing( .fromProgress(0, toProgress: 0.7, loopMode: .playOnce) ) - self.reactionButtonTapped() + likeTapMemeId = meme?.id + likeTapCnt += 1 + meme?.reaction += 1 + meme?.isReaction = true + + throttler.throttle { + self.reactionButtonTapped(likeTapMemeId, likeTapCnt) + likeTapMemeId = nil + likeTapCnt = 0 + } } ) - .disabled(self.isTapLikeButton) .overlay { LottieView(animation: AnimationAsset.kkEffect.animation) .playbackMode(playbackMode) .animationDidFinish { _ in - self.isTapLikeButton = false playbackMode = .paused } .frame(width: 200, height: 200, alignment: .center) @@ -57,7 +65,7 @@ struct RecommendMemeButtonView : View { shareButton(shareButtonTapped) - saveButton(isFarmemed: isFarmemed) { + saveButton(isFarmemed: meme?.isFarmemed ?? false) { saveButtonTapped() } } @@ -67,12 +75,21 @@ struct RecommendMemeButtonView : View { LinearGradient( colors: [ Color.Background.brandsubassistive.opacity(0), - isOverlapView ? Color.Background.brandsubassistive : Color.Background.brandsubassistive.opacity(0) + isOverlapView + ? Color.Background.brandsubassistive + : Color.Background.brandsubassistive.opacity(0) ], startPoint: .top, endPoint: .bottom ) ) + .onChange(of: meme?.id) { + // 쓰로틀링 중에 밈이 변경되면 기다리는 것을 중단하고 바로 서버로 요청 + throttler.cancel() + self.reactionButtonTapped(likeTapMemeId, likeTapCnt) + likeTapMemeId = nil + likeTapCnt = 0 + } } } @@ -110,23 +127,12 @@ func saveButton( } #Preview { - var isReaction: Bool = true - var reactionCnt: Int = 1 - var isFarmemed: Bool = false - return RecommendMemeButtonView( - isReaction: isReaction, - reactionCnt: reactionCnt, - isFarmemed: isFarmemed, + meme: .constant(MemeDetail.mock), isOverlapView: true, - reactionButtonTapped: { - isReaction = true - reactionCnt = +1 - }, + reactionButtonTapped: {_, _ in }, copyButtonTapped: { print("copy~~") }, shareButtonTapped: { print("share~~") }, - saveButtonTapped: { - isFarmemed.toggle() - } + saveButtonTapped: { print("save~~~") } ) } diff --git a/Projects/Features/Recommend/Sources/Presentation/RecommendMemeImageView.swift b/Projects/Features/Recommend/Sources/Presentation/RecommendMemeImageView.swift index 966b99f..54fd5bf 100644 --- a/Projects/Features/Recommend/Sources/Presentation/RecommendMemeImageView.swift +++ b/Projects/Features/Recommend/Sources/Presentation/RecommendMemeImageView.swift @@ -22,7 +22,7 @@ struct RecommendMemeImagesView: View { @State var imageStatusList: [String : Bool] = [:] var memes: [MemeDetail] - var isTagHidden: Bool = false + var isMemeInfoHidden: Bool public var body: some View { VStack(spacing: 0) { @@ -54,26 +54,34 @@ struct RecommendMemeImagesView: View { .recommendSkeleton( isShow: memes.isEmpty, radius: 20, - width: memes.isEmpty ? 270 : .infinity, - height: 310 + width: memes.isEmpty ? 280 : .infinity, + height: 280 ) } + .frame(height: 280) .scrollIndicators(.never) .scrollTargetBehavior(.viewAligned) .scrollPosition(id: $currentMeme) .contentMargins(.horizontal, 60.0) - .padding(.bottom, 20) + .padding(.bottom, 16) - if let currentMeme, isTagHidden == false { - HashTagView(keywords: currentMeme.keywords) - .onTapGesture { - PPACAnalytics.shared - .log( - interaction: .click, - event: .tag, - page: .recommend - ) - } + if let currentMeme { + if(isMemeInfoHidden == false) { + Text(currentMeme.title) + .font(Font.Heading.Small.medium) + .foregroundColor(Color.Text.primary) + .padding(.bottom, 4) + + HashTagView(keywords: currentMeme.keywords) + .onTapGesture { + PPACAnalytics.shared + .log( + interaction: .click, + event: .tag, + page: .recommend + ) + } + } } else { EmptyView() .recommendSkeleton(isShow: true, radius: 4, width: 200, height: 16) @@ -179,7 +187,7 @@ struct RecommendMemeImagesView: View { isReaction: false ) ], - isTagHidden: false + isMemeInfoHidden: false ) .frame(maxWidth: .infinity, maxHeight: .infinity) .background( diff --git a/Projects/Features/Recommend/Sources/Presentation/RecommendRouter.swift b/Projects/Features/Recommend/Sources/Presentation/RecommendRouter.swift index b3b1760..bfd2404 100644 --- a/Projects/Features/Recommend/Sources/Presentation/RecommendRouter.swift +++ b/Projects/Features/Recommend/Sources/Presentation/RecommendRouter.swift @@ -56,6 +56,7 @@ public final class RecommendRouter: Router, RecommendRouting { let getUserInfoUseCase = GetUserInfoUseCaseImpl(userRepository: userRepository) let watchMemeUseCase = WatchMemeUseCaseImpl(repository: memeRepository) let reactToMemeUseCase = ReactToMemeUseCaseImpl(repository: memeRepository) + let sharedMemeUseCase = ShareMemeUseCaseImpl(repository: memeRepository) let bookmarkMemeUseCase = BookmarkMemeUseCaseImpl(repository: memeRepository) let getMemeUseCase = GetMemeDetailUseCaseImpl(repository: memeRepository) @@ -66,7 +67,8 @@ public final class RecommendRouter: Router, RecommendRouting { getUserInfoUseCase: getUserInfoUseCase, watchMemeUseCase: watchMemeUseCase, reactToMemeUseCase: reactToMemeUseCase, - bookmarkMemeUseCase: bookmarkMemeUseCase, + sharedMemeUseCase: sharedMemeUseCase, + bookmarkMemeUseCase: bookmarkMemeUseCase, getMemeDetailUseCase: getMemeUseCase, deepLinkMemeId: deepLinkMemeId ) @@ -87,8 +89,7 @@ public final class RecommendRouter: Router, RecommendRouting { self.childRouters.append(router) router.start() } - - public func showMemeEditorView() { + public func showMemeUploadView() { let router = MemeEditorRouter(navigationController: self.navigationController) self.childRouters.append(router) router.start() diff --git a/Projects/Features/Recommend/Sources/Presentation/RecommendView.swift b/Projects/Features/Recommend/Sources/Presentation/RecommendView.swift index 396b85b..1d478ff 100644 --- a/Projects/Features/Recommend/Sources/Presentation/RecommendView.swift +++ b/Projects/Features/Recommend/Sources/Presentation/RecommendView.swift @@ -52,28 +52,25 @@ public struct RecommendView: View { if viewModel.state.recommendMemeSize > 0 && !viewModel.state.isSuccessFetch { ProgressView() .frame(width: 30, height: 30, alignment: .center) - .padding(.bottom, 20) + .padding(.bottom, 30) } RecommendHeaderView( - userLevel: $viewModel.state.userLevel, - seenMemeCount: $viewModel.state.memeRecommendWatchCount, - recommendMemeSize: $viewModel.state.recommendMemeSize + isLoad: viewModel.state.recommendMemeSize > 0, + uploadButtonTap: { viewModel.dispatch(type: .memeUploadButtonTapped) } ) - // TODO: 종윤쓰 여기 수정 부탁함다 - registerButton - ZStack { VStack(spacing: 0) { RecommendMemeImagesView( currentMeme: $currentMeme, memes: viewModel.state.recommendMemes, - isTagHidden: isOverlapView + isMemeInfoHidden: isOverlapView ) .onReadSize { size in memeImageHeight = size.height + print("memeImageHeight: \(memeImageHeight)") } Spacer() @@ -83,11 +80,9 @@ public struct RecommendView: View { VStack(spacing: 0) { Spacer() - if let currentMeme { + if currentMeme != nil { RecommendMemeButtonView( - isReaction: currentMeme.isReaction, - reactionCnt: currentMeme.reaction, - isFarmemed: currentMeme.isFarmemed, + meme: $currentMeme, isOverlapView: isOverlapView, reactionButtonTapped: reactionButtonTap, copyButtonTapped: copyButtonTap, @@ -101,7 +96,7 @@ public struct RecommendView: View { } .zIndex(2) } - .frame(maxHeight: 457) + .frame(maxHeight: 450) .onReadSize { size in memeContentsHeight = size.height } @@ -137,7 +132,7 @@ public struct RecommendView: View { currentOffsetY = viewModel.state.isSuccessFetch ? .zero : 20 } } - .onChange(of: currentMeme) { + .onChange(of: currentMeme?.id) { if let currentMeme { viewModel.dispatch(type: .showRecommendMeme(meme: currentMeme)) viewModel.logRecommend(interaction: .swipe, event: .meme, meme: nil) @@ -181,28 +176,12 @@ public struct RecommendView: View { ) } - // FIXME: 등록하기 버튼 수정 - private var registerButton: some View { - Button( - action: { - viewModel.dispatch(type: .memeRegisterButtonTapped) - }, - label: { - ZStack { - RoundedRectangle(cornerRadius: 10) - .foregroundStyle(Color.Background.primary) - Text("나도 밈 올리기") - .foregroundStyle(Color.Text.inverse) - } - .frame(width: 130, height: 36) - .padding() - } - ) - } - - private func reactionButtonTap() { + private func reactionButtonTap( + memeId: String?, + tabCount: Int + ) { viewModel.dispatch( - type: .likeButtonTapped(meme: currentMeme) + type: .likeButtonTapped(memeId: memeId, tapCount: tabCount) ) } @@ -253,6 +232,7 @@ public struct RecommendView: View { let getUserInfoUseCase = GetUserInfoUseCaseImpl(userRepository: userRepository) let watchMemeUseCase = WatchMemeUseCaseImpl(repository: memeRepository) let reactToMemeUseCase = ReactToMemeUseCaseImpl(repository: memeRepository) + let sharedMemeUseCase = ShareMemeUseCaseImpl(repository: memeRepository) let bookmarkMemeUseCase = BookmarkMemeUseCaseImpl(repository: memeRepository) let getMemeDetailUseCase = GetMemeDetailUseCaseImpl(repository: memeRepository) @@ -263,6 +243,7 @@ public struct RecommendView: View { getUserInfoUseCase: getUserInfoUseCase, watchMemeUseCase: watchMemeUseCase, reactToMemeUseCase: reactToMemeUseCase, + sharedMemeUseCase: sharedMemeUseCase, bookmarkMemeUseCase: bookmarkMemeUseCase, getMemeDetailUseCase: getMemeDetailUseCase, deepLinkMemeId: PassthroughSubject() diff --git a/Projects/Features/Recommend/Sources/Presentation/RecommendViewModel.swift b/Projects/Features/Recommend/Sources/Presentation/RecommendViewModel.swift index 2e80670..7226069 100644 --- a/Projects/Features/Recommend/Sources/Presentation/RecommendViewModel.swift +++ b/Projects/Features/Recommend/Sources/Presentation/RecommendViewModel.swift @@ -17,7 +17,7 @@ import PPACAnalytics public protocol RecommendRouting: AnyObject { func showShareView(items: [Any]) func showMemeDetailView(meme: MemeDetail) - func showMemeEditorView() + func showMemeUploadView() } public final class RecommendViewModel: ViewModelType, ObservableObject { @@ -25,11 +25,11 @@ public final class RecommendViewModel: ViewModelType, ObservableObject { public enum Action { case viewInitialized case showRecommendMeme(meme: MemeDetail?) - case likeButtonTapped(meme: MemeDetail?) + case likeButtonTapped(memeId: String?, tapCount: Int) case copyButtonTapped(meme: MemeDetail?) case shareButtonTapped(meme: MemeDetail?) case farmemeButtonTapped(meme: MemeDetail?) - case memeRegisterButtonTapped + case memeUploadButtonTapped } public struct State { @@ -38,7 +38,7 @@ public final class RecommendViewModel: ViewModelType, ObservableObject { var userLevel: Int var memeRecommendWatchCount: Int var isSuccessFetch: Bool - var isSuccessMemeRegister: Bool = false + var isSuccessMemeUpload: Bool = false } weak var router: RecommendRouting? @@ -48,6 +48,7 @@ public final class RecommendViewModel: ViewModelType, ObservableObject { private let getUserInfoUseCase: GetUserInfoUseCase private let watchMemeUseCase: WatchMemeUseCase private let reactToMemeUseCase: ReactToMemeUseCase + private let sharedMemeUseCase: ShareMemeUseCase private let bookmarkMemeUseCase: BookmarkMemeUseCase private let getMemeDetailUseCase: GetMemeDetailUseCase private let deepLinkMemeId: PassthroughSubject @@ -59,6 +60,7 @@ public final class RecommendViewModel: ViewModelType, ObservableObject { getUserInfoUseCase: GetUserInfoUseCase, watchMemeUseCase: WatchMemeUseCase, reactToMemeUseCase: ReactToMemeUseCase, + sharedMemeUseCase: ShareMemeUseCase, bookmarkMemeUseCase: BookmarkMemeUseCase, getMemeDetailUseCase: GetMemeDetailUseCase, deepLinkMemeId: PassthroughSubject @@ -68,6 +70,7 @@ public final class RecommendViewModel: ViewModelType, ObservableObject { self.getUserInfoUseCase = getUserInfoUseCase self.watchMemeUseCase = watchMemeUseCase self.reactToMemeUseCase = reactToMemeUseCase + self.sharedMemeUseCase = sharedMemeUseCase self.bookmarkMemeUseCase = bookmarkMemeUseCase self.getMemeDetailUseCase = getMemeDetailUseCase self.deepLinkMemeId = deepLinkMemeId @@ -79,6 +82,8 @@ public final class RecommendViewModel: ViewModelType, ObservableObject { isSuccessFetch: false ) bind() + + UserInfo.shared.deviceId = "uni-test4" } public func dispatch(type: Action) { @@ -88,16 +93,16 @@ public final class RecommendViewModel: ViewModelType, ObservableObject { await getRecommendAndUser() case .showRecommendMeme(let meme): await postShownMeme(meme: meme) - case .likeButtonTapped(let meme): - await postReaction(meme: meme) + case .likeButtonTapped(let memeId, let tapCount): + await postReaction(memeId: memeId, tapCount: tapCount) case .copyButtonTapped(let meme): await copyImage(meme: meme) case .shareButtonTapped(let meme): await showShareSheet(meme: meme) case .farmemeButtonTapped(let meme): await saveMeme(meme: meme) - case .memeRegisterButtonTapped: - router?.showMemeEditorView() + case .memeUploadButtonTapped: + router?.showMemeUploadView() } } } @@ -144,7 +149,7 @@ private extension RecommendViewModel { let recommendMemes = try await getRecommendMemesUseCase.execute(size: recommendMemeSize) let user = try await getUserInfoUseCase.execute() print("👍memeids: \(recommendMemes.map { $0.id })") - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + DispatchQueue.main.asyncAfter(deadline: .now()) { self.state.recommendMemes = recommendMemes self.state.recommendMemeSize = recommendMemes.count self.state.userLevel = user.level @@ -170,16 +175,20 @@ private extension RecommendViewModel { } } - func postReaction(meme: MemeDetail?) async { - guard let meme else { return } + @MainActor + func postReaction(memeId: String?, tapCount: Int) async { + guard let memeId else { return } do { - try await reactToMemeUseCase.execute(memeId: meme.id) + let memeReactionCount = try await reactToMemeUseCase.execute( + memeId: memeId, + count: tapCount + ) - if let index = self.state.recommendMemes.firstIndex(where: { $0.id == meme.id }) { + if let index = self.state.recommendMemes.firstIndex(where: { $0.id == memeId }) { self.state.recommendMemes[index].isReaction = true - self.state.recommendMemes[index].reaction += 1 + self.state.recommendMemes[index].reaction = memeReactionCount + self.logRecommend(event: .reaction, meme: self.state.recommendMemes[index]) } - self.logRecommend(event: .reaction, meme: meme) } catch { debugPrint("Failed post recation : \(error)") } @@ -195,7 +204,7 @@ private extension RecommendViewModel { guard let image = UIImage(data: data) else { return } - + UIPasteboard.general.image = image self.logRecommend(event: .copy, meme: meme) } catch { @@ -205,13 +214,19 @@ private extension RecommendViewModel { @MainActor func showShareSheet(meme: MemeDetail?) async { - guard let meme else { return } - guard let memeId = self.state.recommendMemes.filter({ $0.imageUrlString == meme.imageUrlString }).first?.id else { - return - } + guard let meme else { return } + guard let memeId = self.state.recommendMemes + .filter({ $0.imageUrlString == meme.imageUrlString }) + .first?.id else { return} + let deeplinkUrl = "https://farmeme.onelink.me/RtpU/y09dosru?deep_link_value=\(memeId)" router?.showShareView(items: [deeplinkUrl]) - self.logRecommend(event: .share, meme: meme) + do { + try await sharedMemeUseCase.execute(memeId: memeId) + self.logRecommend(event: .share, meme: meme) + } catch { + debugPrint("Failed to load image data: \(error)") + } } func saveMeme(meme: MemeDetail?) async { @@ -242,5 +257,5 @@ private extension RecommendViewModel { } } } - + } diff --git a/Projects/Features/Recommend/Sources/Presentation/View/LikeButton.swift b/Projects/Features/Recommend/Sources/Presentation/View/LikeButton.swift index 5453125..a629b5d 100644 --- a/Projects/Features/Recommend/Sources/Presentation/View/LikeButton.swift +++ b/Projects/Features/Recommend/Sources/Presentation/View/LikeButton.swift @@ -48,7 +48,6 @@ public struct LikeButton: View { playbackMode = .playing( .fromProgress(0, toProgress: 1, loopMode: .playOnce) ) - self.didTapped() } } @@ -84,12 +83,10 @@ public struct LikeButton: View { } #Preview { - var count: Int = 3 - return VStack { LikeButton( isReaction: false, - reactionCount: count, + reactionCount: 0, didTapped: { print("AA") } @@ -97,7 +94,7 @@ public struct LikeButton: View { LikeButton( isReaction: true, - reactionCount: count, + reactionCount: 1, didTapped: { print("AA") } diff --git a/Projects/Features/Recommend/Sources/Presentation/View/MemeImageView.swift b/Projects/Features/Recommend/Sources/Presentation/View/MemeImageView.swift index def37af..ac5402b 100644 --- a/Projects/Features/Recommend/Sources/Presentation/View/MemeImageView.swift +++ b/Projects/Features/Recommend/Sources/Presentation/View/MemeImageView.swift @@ -14,24 +14,24 @@ struct MemeImageView: View { let isDimmed: Bool @State var image: URL? = nil - @Binding var isLoadingImage: Bool public var body: some View { Rectangle() - .frame(width: 270, height: 310) + .frame(width: 280, height: 280) .cornerRadius(20) .foregroundColor(.black.opacity(isLoadingImage ? 1 : 0)) .overlay { KFImage(URL(string: imageUrl)) .placeholder { EmptyView() - .recommendSkeleton(isShow: true, radius: 20, width: 270, height: 310) + .recommendSkeleton(isShow: true, radius: 20, width: 280, height: 280) } .onSuccess { _ in isLoadingImage = true } .resizable() + .animation(nil, value: UUID()) .aspectRatio(contentMode: .fit) if isDimmed && isLoadingImage { @@ -39,6 +39,7 @@ struct MemeImageView: View { .foregroundStyle(Color.Background.dimmer) } } + .clipShape(RoundedRectangle(cornerRadius: 20)) .scrollTransition { content, phase in content .offset(x: phase.value * -3) @@ -61,6 +62,7 @@ struct MemeImageBorderView: View { Color.Border.primary.opacity(isLoadingImage ? 1 : 0), lineWidth: 2 ) + .frame(width: 280, height: 280) .scrollTransition { content, phase in content .offset(x: phase.value * -3) @@ -74,6 +76,6 @@ struct MemeImageBorderView: View { imageUrl: "https://ppac-meme.s3.ap-northeast-2.amazonaws.com/17204513204087.jpg", // imageUrl: "", isDimmed: false, - isLoadingImage: .constant(false) + isLoadingImage: .constant(true) ) } diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Home_Logo.imageset/home-logo.svg b/Projects/ResourceKit/Resources/Icon.xcassets/Home_Logo.imageset/home-logo.svg index f5c3ba6..6982efe 100644 --- a/Projects/ResourceKit/Resources/Icon.xcassets/Home_Logo.imageset/home-logo.svg +++ b/Projects/ResourceKit/Resources/Icon.xcassets/Home_Logo.imageset/home-logo.svg @@ -1,33 +1,33 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Upload.imageset/Contents.json b/Projects/ResourceKit/Resources/Icon.xcassets/Upload.imageset/Contents.json new file mode 100644 index 0000000..1fa43cd --- /dev/null +++ b/Projects/ResourceKit/Resources/Icon.xcassets/Upload.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Stroke.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Upload.imageset/Stroke.svg b/Projects/ResourceKit/Resources/Icon.xcassets/Upload.imageset/Stroke.svg new file mode 100644 index 0000000..8fca6b2 --- /dev/null +++ b/Projects/ResourceKit/Resources/Icon.xcassets/Upload.imageset/Stroke.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Projects/ResourceKit/Resources/Lottie/Upload.lottie.lottie b/Projects/ResourceKit/Resources/Lottie/Upload.lottie.lottie new file mode 100644 index 0000000..9e4900a --- /dev/null +++ b/Projects/ResourceKit/Resources/Lottie/Upload.lottie.lottie @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.1","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":0,"op":180.00000733155,"w":390,"h":108,"nm":"Comp 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[7,0,0],"ix":2},"a":{"a":0,"k":[-195.356,-54.887,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[375,93],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":25,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[40]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":60,"s":[40]},{"t":72.0000029326201,"s":[20]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[160]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[350]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":50,"s":[420]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":60,"s":[520]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":72,"s":[710]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":99,"s":[780]},{"t":113.000004602584,"s":[844]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.996078431373,0.341176470588,0.227450980392,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[4]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[3.6]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":106,"s":[4]},{"t":113.000004602584,"s":[0]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-7.356,-0.887],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180.00000733155,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Layer 2 Outlines 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[195,54,0],"ix":2},"a":{"a":0,"k":[195,54,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.15,0],[0,0],[0,3.15],[0,0],[-3.15,0],[0,0],[0,-3.15],[0,0]],"o":[[0,0],[-3.15,0],[0,0],[0,-3.15],[0,0],[3.15,0],[0,0],[0,3.15]],"v":[[10.184,18.93],[-10.184,18.93],[-15.911,13.202],[-15.911,-13.202],[-10.184,-18.93],[10.184,-18.93],[15.911,-13.202],[15.911,13.202]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[68.305,54.123],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180.00000733155,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Layer 4 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[195,54,0],"ix":2},"a":{"a":0,"k":[195,54,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.103,0.735],[0,0],[0.087,0.047],[0.145,0.045],[0.25,-0.024],[0.226,-0.067],[0.143,-0.077],[0.077,-0.054],[0,0],[-0.735,-1.102],[-1.104,0.736],[0,0],[0,0],[-1.325,0],[0,1.325],[0,0],[0,0],[-0.735,1.103]],"o":[[0,0],[-0.082,-0.057],[-0.132,-0.069],[-0.233,-0.073],[-0.226,-0.001],[-0.158,0.047],[-0.081,0.044],[0,0],[-1.103,0.735],[0.736,1.103],[0,0],[0,0],[0,1.325],[1.325,0],[0,0],[0,0],[1.103,0.735],[0.735,-1.102]],"v":[[7.331,-3.985],[1.378,-7.954],[1.125,-8.109],[0.709,-8.281],[0,-8.388],[-0.687,-8.288],[-1.139,-8.101],[-1.377,-7.954],[-7.331,-3.985],[-7.997,-0.657],[-4.669,0.009],[-2.4,-1.504],[-2.4,6.012],[0,8.412],[2.4,6.012],[2.4,-1.504],[4.669,0.009],[7.997,-0.657]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[68.499,53.988],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180.00000733155,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Layer 3 Outlines 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[195,54,0],"ix":2},"a":{"a":0,"k":[195,54,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16.898,1.825],[-16.898,1.825],[-16.898,-1.825],[16.898,-1.825]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[251.958,51.319],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[2.29,2.74],[-2.29,2.74],[-2.29,-2.74],[2.29,-2.74]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[251.823,48.829],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.281],[5.987,0],[0,2.953],[-5.987,0]],"o":[[0,2.953],[-5.987,0],[0,-3.193],[5.987,0]],"v":[[11.384,-0.135],[0.038,4.607],[-11.384,-0.135],[0.038,-4.607]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[251.92,40.974],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[10.147,4.829],[-10.148,4.829],[-10.148,-4.829],[10.147,-4.829]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[207.236,63.814],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.859,6.264],[-6.859,6.264],[-6.859,-6.264],[6.859,-6.264]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[199.581,44.552],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[13.002,-6.152],[0,0],[-0.801,8.429],[0,0],[0,0]],"o":[[0,11.033],[0,0],[9.699,-4.614],[0,0],[0,0],[0,0]],"v":[[9.823,-13.289],[-7.445,13.289],[-9.824,9.639],[5.188,-9.68],[-7.937,-9.68],[-7.937,-13.289]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[317.729,51.299],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[2.297,18.539],[-2.296,18.539],[-2.296,-18.539],[2.297,-18.539]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[336.328,52.611],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[-5.763,0.984],[0,0],[6.645,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[5.847,-0.041],[0,0],[-6.318,1.107],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[6.582,-12.551],[6.582,1.353],[-6.174,1.353],[-6.174,8.818],[10.479,7.506],[10.89,11.238],[-7.608,12.551],[-10.848,12.551],[-10.848,-2.256],[1.947,-2.256],[1.947,-8.86],[-10.89,-8.86],[-10.89,-12.551]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[284.259,49.863],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[2.256,18.559],[-2.256,18.559],[-2.256,-18.559],[2.256,-18.559]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[299.988,52.632],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[12.284,-7.65],[12.284,1.415],[-8.797,1.415],[-8.797,4.163],[13.31,4.163],[13.31,7.65],[-13.269,7.65],[-13.269,-1.826],[7.814,-1.826],[7.814,-4.204],[-13.31,-4.204],[-13.31,-7.65]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[252.349,63.173],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[2.297,10.5],[-2.297,10.5],[-2.297,-10.5],[2.297,-10.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[217.383,44.613],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 11","np":2,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.166,-0.861],[13.166,2.748],[2.174,2.748],[2.174,11.239],[16.939,11.239],[16.939,14.971],[-16.939,14.971],[-16.939,11.239],[-2.338,11.239],[-2.338,2.748],[-12.838,2.748],[-12.838,-14.971],[12.879,-14.971],[12.879,-11.32],[-8.285,-11.32],[-8.285,-0.861]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[159.469,51.791],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 12","np":2,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-5.578,1.169],[0,0],[5.497,0],[0,0],[0,0]],"o":[[0,0],[4.942,-0.082],[0,0],[-6.234,1.23],[0,0],[0,0],[0,0]],"v":[[-5.885,-12.12],[-5.885,8.347],[9.906,6.665],[10.398,10.521],[-7.281,12.12],[-10.398,12.12],[-10.398,-12.12]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[117.94,49.802],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 13","np":2,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[5.024,-4.184],[5.024,-0.452],[-0.472,-0.452],[-0.472,18.498],[-5.025,18.498],[-5.025,-18.498],[-0.472,-18.498],[-0.472,-4.184]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[135.741,52.571],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 14","np":2,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180.00000733155,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Layer 1 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[195,54,0],"ix":2},"a":{"a":0,"k":[195,54,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-16.569,0],[0,0],[0,-16.569],[0,0],[16.569,0],[0,0],[0,16.569],[0,0]],"o":[[0,0],[16.569,0],[0,0],[0,16.569],[0,0],[-16.569,0],[0,0],[0,-16.569]],"v":[[-165,-54],[165,-54],[195,-24],[195,24],[165,54],[-165,54],[-195,24],[-195,-24]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.117647066303,0.133333333333,0.152941176471,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[195,54],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180.00000733155,"st":0,"bm":0}],"markers":[]} \ No newline at end of file