diff --git a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj
index 531149f4..f513f5f4 100644
--- a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj
+++ b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj
@@ -1703,7 +1703,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 2024.0208.0315;
+ CURRENT_PROJECT_VERSION = 2024.0312.0041;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8Q4H7X3Q58;
GENERATE_INFOPLIST_FILE = NO;
@@ -1726,7 +1726,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.runnect.Runnect-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
- "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.runnect.Runnect-iOS";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.runnect-Runnect-iOS";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
@@ -1747,7 +1747,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 2024.0208.0315;
+ CURRENT_PROJECT_VERSION = 2024.0312.0041;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8Q4H7X3Q58;
GENERATE_INFOPLIST_FILE = NO;
@@ -1770,7 +1770,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.runnect.Runnect-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
- "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.runnect.Runnect-iOS";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.runnect-Runnect-iOS";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
diff --git a/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIViewController+.swift b/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIViewController+.swift
index 547d088a..1ee0ea48 100644
--- a/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIViewController+.swift
+++ b/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIViewController+.swift
@@ -8,6 +8,7 @@
import UIKit
import SnapKit
+import FirebaseDynamicLinks
extension UIViewController {
@@ -89,10 +90,84 @@ extension UIViewController {
}
extension UIViewController {
+
+ /**
+ ### Description: 공유 기능에 해당하는 정보를 넣어줍니다.
+ 2025년 8월 25일에 동적링크는 만기 됩니다.
+
+ - courseTitle : 타이틀 이름
+ - courseId : 코스 아이디
+ - courseImageURL : 코스 사진
+ - minimumAppVersion : 공유 기능을 사용할 수 있는 최소 버전
+ - descriptionText : 내용
+ - parameter : 공유 기능에 필요한 파라미터
+ */
+ /// 공유 기능 메서드
+ ///
+ func shareCourse(
+ courseTitle: String,
+ courseId: Int,
+ courseImageURL: String,
+ minimumAppVersion: String,
+ descriptionText: String? = nil,
+ parameter: String
+ ) {
+ let dynamicLinksDomainURIPrefix = "https://rnnt.page.link"
+ let courseParameter = parameter
+ guard let link = URL(string: "\(dynamicLinksDomainURIPrefix)/?\(courseParameter)=\(courseId)") else {
+ print("Invalid link.")
+ return
+ }
+
+ guard let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix) else {
+ print("Failed to create link builder.")
+ return
+ }
+
+ linkBuilder.iOSParameters = DynamicLinkIOSParameters(bundleID: "com.runnect.Runnect-iOS")
+ linkBuilder.iOSParameters?.appStoreID = "1663884202"
+ linkBuilder.iOSParameters?.minimumAppVersion = minimumAppVersion
+
+ linkBuilder.androidParameters = DynamicLinkAndroidParameters(packageName: "com.runnect.runnect")
+
+ linkBuilder.socialMetaTagParameters = DynamicLinkSocialMetaTagParameters()
+ linkBuilder.socialMetaTagParameters?.imageURL = URL(string: courseImageURL)
+ linkBuilder.socialMetaTagParameters?.title = courseTitle
+ linkBuilder.socialMetaTagParameters?.descriptionText = descriptionText ?? ""
+
+ linkBuilder.shorten { [weak self] url, _, error in
+ guard let shortDynamicLink = url?.absoluteString else {
+ if let error = error {
+ print("Error shortening dynamic link: \(error)")
+ }
+ return
+ }
+
+ print("Short URL is: \(shortDynamicLink)")
+ DispatchQueue.main.async {
+ self?.presentShareActivity(with: shortDynamicLink)
+ }
+ }
+ }
+
+ private func presentShareActivity(with url: String) {
+ let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil)
+ activityVC.popoverPresentationController?.sourceView = self.view
+ self.present(activityVC, animated: true, completion: nil)
+ }
+}
+
+extension UIViewController {
+ /**
+ - Description: 뷰컨에서 GA(구글 애널리틱스) 스크린 , 버튼 이벤트 사용 사는 메서드 입니다.
+ */
+
+ /// 스크린 이벤트
func analyze(screenName: String) {
GAManager.shared.logEvent(eventType: .screen(screenName: screenName))
}
+ /// 버튼 이벤트
func analyze(buttonName: String) {
GAManager.shared.logEvent(eventType: .button(buttonName: buttonName))
}
diff --git a/Runnect-iOS/Runnect-iOS/Global/Supports/SceneDelegate.swift b/Runnect-iOS/Runnect-iOS/Global/Supports/SceneDelegate.swift
index 455a21bb..954dc637 100644
--- a/Runnect-iOS/Runnect-iOS/Global/Supports/SceneDelegate.swift
+++ b/Runnect-iOS/Runnect-iOS/Global/Supports/SceneDelegate.swift
@@ -12,11 +12,15 @@ import FirebaseDynamicLinks
import FirebaseCore
import FirebaseCoreInternal
+// 들어온 링크가 공유된 코스인지, 개인 보관함에 있는 코스인지 나타내기 위한 타입입니다.
+enum CourseType {
+ case publicCourse, privateCourse
+}
+
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
-
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
@@ -39,30 +43,33 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if let incomingURL = userActivity.webpageURL {
- let linkHandled = DynamicLinks.dynamicLinks()
+ DynamicLinks.dynamicLinks()
.handleUniversalLink(incomingURL) { dynamicLink, error in
- if let courseId = self.handleDynamicLink(dynamicLink) {
- guard let _ = (scene as? UIWindowScene) else { return }
+ if let (courseType, courseId) = self.handleDynamicLink(dynamicLink) {
+ guard let windowScene = scene as? UIWindowScene else { return }
+ let window = UIWindow(windowScene: windowScene)
+ let navigationController = UINavigationController()
- if let windowScene = scene as? UIWindowScene {
- let window = UIWindow(windowScene: windowScene)
-
+ switch courseType {
+ case .publicCourse:
let courseDetailVC = CourseDetailVC()
- courseDetailVC.getUploadedCourseDetail(courseId: Int(courseId))
-
- let tabBarController = TabBarController()
- let navigationController = UINavigationController(rootViewController: tabBarController)
- navigationController.navigationBar.isHidden = true
+ courseDetailVC.getUploadedCourseDetail(courseId: courseId)
navigationController.pushViewController(courseDetailVC, animated: false)
-
- // 코스 발견 view 로 이동
- tabBarController.selectedIndex = 2
- window.rootViewController = navigationController
- window.makeKeyAndVisible()
- self.window = window
-
+ case .privateCourse:
+ let privateCourseDetailVC = RunningWaitingVC()
+ privateCourseDetailVC.setData(courseId: courseId, publicCourseId: nil)
+ navigationController.pushViewController(privateCourseDetailVC, animated: false)
}
+
+ let tabBarController = TabBarController()
+ navigationController.navigationBar.isHidden = true
+ navigationController.viewControllers = [tabBarController, navigationController.viewControllers.last].compactMap { $0 }
+
+ tabBarController.selectedIndex = 2
+ window.rootViewController = navigationController
+ window.makeKeyAndVisible()
+ self.window = window
}
}
}
@@ -106,17 +113,26 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// to restore the scene back to its current state.
}
- func handleDynamicLink(_ dynamicLink: DynamicLink?) -> String? {
+ func handleDynamicLink(_ dynamicLink: DynamicLink?) -> (courseType: CourseType, courseId: Int)? {
if let dynamicLink = dynamicLink, let url = dynamicLink.url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems {
+ var courseId: Int?
+ var courseType: CourseType?
+
for item in queryItems {
- if item.name == "courseId", let courseId = item.value {
- // 동적링크 핸들링 하여 courseId 추출
-
- return courseId
+ if item.name == "courseId", let id = item.value, let idInt = Int(id) {
+ courseId = idInt
+ courseType = .publicCourse
+ } else if item.name == "privateCourseId", let id = item.value, let idInt = Int(id) {
+ courseId = idInt
+ courseType = .privateCourse
}
}
+
+ if let courseId = courseId, let courseType = courseType {
+ return (courseType, courseId)
+ }
}
return nil
}
diff --git a/Runnect-iOS/Runnect-iOS/Info.plist b/Runnect-iOS/Runnect-iOS/Info.plist
index d7b63c1f..104548fc 100644
--- a/Runnect-iOS/Runnect-iOS/Info.plist
+++ b/Runnect-iOS/Runnect-iOS/Info.plist
@@ -19,7 +19,9 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 2.0.0
+ 2.0.1
+ ITSAppUsesNonExemptEncryption
+
CFBundleURLTypes
@@ -44,7 +46,7 @@
CFBundleVersion
- 2024.0208.0315
+ 2024.0312.0041
LSApplicationQueriesSchemes
kakaokompassauth
diff --git a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseStorageDto/ResponseDto/PrivateCourseResponseDto.swift b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseStorageDto/ResponseDto/PrivateCourseResponseDto.swift
index b0e5d0d1..60cf57e0 100644
--- a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseStorageDto/ResponseDto/PrivateCourseResponseDto.swift
+++ b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseStorageDto/ResponseDto/PrivateCourseResponseDto.swift
@@ -17,6 +17,7 @@ struct PrivateCourseResponseDto: Codable {
struct PrivateCourse: Codable {
let id: Int
+ let isNowUser: Bool?
let title: String
let image, createdAt: String
let distance: Float?
diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift
index a829a2b4..374a2559 100644
--- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift
+++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift
@@ -2,7 +2,7 @@
// CourseDetailVC.swift
// Runnect-iOS
//
-// Created by 몽이 누나 on 2023/01/05.
+// Created by 이명진 on 2023/10/09.
//
import UIKit
@@ -13,9 +13,6 @@ import NMapsMap
import Moya
import SafariServices
import KakaoSDKCommon
-import FirebaseCore
-import FirebaseDynamicLinks
-import KakaoSDKShare
import KakaoSDKTemplate
import DropDown
@@ -167,9 +164,7 @@ extension CourseDetailVC {
scrapCourse(scrapTF: !sender.isSelected)
delegate?.didUpdateScrapState(publicCourseId: publicCourseId, isScrapped: !sender.isSelected) /// 코스 발견 UI Update 부분
- marathonDelegate?.didUpdateMarathonScrapState(publicCourseId: publicCourseId, isScrapped: !sender.isSelected) // 마라톤 코스 UI Update 부분
-
- /// print("CourseDetailVC 스크랩 탭🔥publicCourseId=\(publicCourseId), isScrapped은 \(!sender.isSelected) 요렇게 변경 ")
+ marathonDelegate?.didUpdateMarathonScrapState(publicCourseId: publicCourseId, isScrapped: !sender.isSelected) // 마라톤 코스 UI
}
@objc private func shareButtonTapped() {
@@ -177,58 +172,18 @@ extension CourseDetailVC {
return
}
- analyze(buttonName: GAEvent.Button.clickShare)
-
let publicCourse = model.publicCourse
- let title = publicCourse.title
- let courseId = publicCourse.id // primaryKey
- let description = publicCourse.description
- let courseImage = publicCourse.image
-
- let dynamicLinksDomainURIPrefix = "https://rnnt.page.link"
- guard let link = URL(string: "\(dynamicLinksDomainURIPrefix)/?courseId=\(courseId)") else {
- return
- }
-
- print("‼️link= \(link)")
-
- guard let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix) else {
- return
- }
-
- linkBuilder.iOSParameters = DynamicLinkIOSParameters(bundleID: "com.runnect.Runnect-iOS")
- linkBuilder.iOSParameters?.appStoreID = "1663884202"
- linkBuilder.iOSParameters?.minimumAppVersion = "1.0.4"
-
- linkBuilder.androidParameters = DynamicLinkAndroidParameters(packageName: "com.runnect.runnect")
- linkBuilder.socialMetaTagParameters = DynamicLinkSocialMetaTagParameters()
- linkBuilder.socialMetaTagParameters?.imageURL = URL(string: courseImage)
- linkBuilder.socialMetaTagParameters?.title = title
- linkBuilder.socialMetaTagParameters?.descriptionText = description
-
- guard let longDynamicLink = linkBuilder.url else {
- return
- }
- print("The long URL is: \(longDynamicLink)")
+ analyze(buttonName: GAEvent.Button.clickShare)
- /// 짧은 Dynamic Link로 변환하는 부분 입니다.
- linkBuilder.shorten { [weak self] url, _, error in // warning 파라미터 와일드 카드
- guard let shortDynamicLink = url?.absoluteString else {
- if let error = error {
- print("❌Error shortening dynamic link: \(error)")
- }
- return
- }
-
- print("🔥The short URL is: \(shortDynamicLink)")
-
- DispatchQueue.main.async {
- let activityVC = UIActivityViewController(activityItems: [shortDynamicLink], applicationActivities: nil)
- activityVC.popoverPresentationController?.sourceView = self?.view
- self?.present(activityVC, animated: true, completion: nil)
- }
- }
+ self.shareCourse(
+ courseTitle: publicCourse.title,
+ courseId: publicCourse.id,
+ courseImageURL: publicCourse.image,
+ minimumAppVersion: "1.0.4",
+ descriptionText: publicCourse.description,
+ parameter: "courseId"
+ )
}
@objc private func pushToUserProfileVC() {
@@ -479,10 +434,6 @@ extension CourseDetailVC {
$0.width.height.equalTo(37)
}
-// profileNameLabel.snp.makeConstraints {
-// $0.leading.equalTo(profileImageView.snp.trailing).offset(12)
-// }
-
firstHorizontalDivideLine.snp.makeConstraints {
$0.top.equalTo(mapImageView.snp.bottom).offset(62)
$0.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(14)
diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift
index f39f141b..48daf273 100644
--- a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift
+++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift
@@ -107,9 +107,8 @@ extension CourseStorageVC {
guard let self = self else { return }
analyze(buttonName: GAEvent.Button.clickScrapPageStartCourse) // 코스 발견_스크랩코스 상세페이지 시작하기 Event
- let title = self.privateCourseList[index].title
let runningWaitingVC = RunningWaitingVC()
- runningWaitingVC.setData(courseId: self.privateCourseList[index].id, publicCourseId: nil, courseTitle: title)
+ runningWaitingVC.setData(courseId: self.privateCourseList[index].id, publicCourseId: nil)
/// 코스 이름을 여기서 가져오는 로직
runningWaitingVC.hidesBottomBarWhenPushed = true
diff --git a/Runnect-iOS/Runnect-iOS/Presentation/Running/VC/RunningWaitingVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/Running/VC/RunningWaitingVC.swift
index 99bf636f..9c8de928 100644
--- a/Runnect-iOS/Runnect-iOS/Presentation/Running/VC/RunningWaitingVC.swift
+++ b/Runnect-iOS/Runnect-iOS/Presentation/Running/VC/RunningWaitingVC.swift
@@ -13,6 +13,8 @@ import Moya
import SnapKit
import Then
+import FirebaseDynamicLinks
+
final class RunningWaitingVC: UIViewController {
// MARK: - Properties
@@ -28,7 +30,12 @@ final class RunningWaitingVC: UIViewController {
// MARK: - UI Components
- private lazy var naviBar = CustomNavigationBar(self, type: .titleWithLeftButton).setTitle(courseTitle ?? "Test Code")
+ private lazy var naviBar = CustomNavigationBar(self, type: .titleWithLeftButton)
+
+ private let shareButton = UIButton(type: .system).then {
+ $0.setImage(ImageLiterals.icShareButton, for: .normal)
+ $0.tintColor = .g1
+ }
private let moreButton = UIButton(type: .system).then {
$0.setImage(ImageLiterals.icMore, for: .normal)
@@ -81,10 +88,9 @@ final class RunningWaitingVC: UIViewController {
// MARK: - Methods
extension RunningWaitingVC {
- func setData(courseId: Int, publicCourseId: Int?, courseTitle: String) {
+ func setData(courseId: Int, publicCourseId: Int?) {
self.courseId = courseId
self.publicCourseId = publicCourseId
- self.courseTitle = courseTitle
getCourseDetail(courseId: courseId)
}
@@ -92,10 +98,16 @@ extension RunningWaitingVC {
private func setCourseData(courseModel: Course) {
self.courseModel = courseModel
+ guard let isMyCourse = courseModel.isNowUser else { return }
+ self.isMyCourse(courseOwner: isMyCourse)
+
+ self.courseTitle = courseModel.title
+ self.naviBar.setTitle(self.courseTitle ?? "타이틀 없음")
+
guard let path = courseModel.path, let distance = courseModel.distance else { return }
let locations = path.map { NMGLatLng(lat: $0[0], lng: $0[1]) }
self.makePath(locations: locations)
- self.distanceLabel.text = String(distance)
+ self.distanceLabel.text = String(format: "%.1f", distance)
}
private func makePath(locations: [NMGLatLng]) {
@@ -104,7 +116,14 @@ extension RunningWaitingVC {
private func setAddTarget() {
self.startButton.addTarget(self, action: #selector(startButtonDidTap), for: .touchUpInside)
- moreButton.addTarget(self, action: #selector(moreButtonDidTap), for: .touchUpInside)
+ self.moreButton.addTarget(self, action: #selector(moreButtonDidTap), for: .touchUpInside)
+ self.shareButton.addTarget(self, action: #selector(shareButtonDidTap), for: .touchUpInside)
+ }
+
+ private func isMyCourse(courseOwner: Bool) {
+ print("💪💪💪💪💪💪💪💪💪💪💪💪💪💪💪💪💪💪")
+ self.shareButton.isHidden = !courseOwner
+ self.moreButton.isHidden = !courseOwner
}
}
@@ -127,8 +146,24 @@ extension RunningWaitingVC {
self.navigationController?.pushViewController(countDownVC, animated: true)
}
+ @objc private func shareButtonDidTap() {
+ guard let model = self.courseModel else {
+ return
+ }
+ analyze(buttonName: GAEvent.Button.clickShare)
+
+ self.shareCourse(
+ courseTitle: model.title,
+ courseId: model.id,
+ courseImageURL: model.image,
+ minimumAppVersion: "2.0.1",
+ descriptionText: "이 코스는 링크로만 들어올 수 있어요!",
+ parameter: "privateCourseId"
+ )
+ }
+
@objc private func moreButtonDidTap() {
- guard let courseModel = self.courseModel else {return}
+ guard let courseModel = self.courseModel else { return }
let items = ["수정하기", "삭제하기"]
let imageArray: [UIImage] = [ImageLiterals.icModify, ImageLiterals.icRemove]
@@ -171,6 +206,7 @@ extension RunningWaitingVC {
private func setLayout() {
view.addSubviews(naviBar,
moreButton,
+ shareButton,
mapView,
distanceContainerView,
startButton)
@@ -182,10 +218,17 @@ extension RunningWaitingVC {
view.bringSubviewToFront(naviBar)
+ shareButton.snp.makeConstraints {
+ $0.trailing.equalTo(moreButton.snp.leading)
+ $0.centerY.equalTo(naviBar)
+ }
+
moreButton.snp.makeConstraints {
$0.trailing.equalTo(self.view.safeAreaLayoutGuide)
$0.centerY.equalTo(naviBar)
}
+
+ view.bringSubviewToFront(shareButton)
view.bringSubviewToFront(moreButton)
mapView.snp.makeConstraints {
diff --git a/Runnect-iOS/fastlane/Matchfile b/Runnect-iOS/fastlane/Matchfile
index 55ed04f7..671dbbf8 100644
--- a/Runnect-iOS/fastlane/Matchfile
+++ b/Runnect-iOS/fastlane/Matchfile
@@ -1,10 +1,10 @@
git_url("https://github.com/thingineeer/fastlane-match-runnect")
-# git_branch("Runnect") 수정 금지
+git_branch("master")
storage_mode("git")
type("development") # The default type, can be: appstore, adhoc, enterprise or development
-# app_identifier("com.runnect.Runnect-iOS")
+app_identifier("com.runnect.Runnect-iOS")
# username("user@fastlane.tools") # Your Apple Developer Portal username
# For all available options run `fastlane match --help`