Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a6ce5f4
chore: add applink url
Siwon-L Aug 23, 2026
9fdd7d2
refactor: generalize DeepLinkManager.setDestination to setQuery
Siwon-L Aug 23, 2026
d83710f
feat: Handle Universal Link for invitation flow
Siwon-L Aug 23, 2026
a06f0ed
feat: Add agencyID-based overload to ConfirmCertificateCodeUseCase
Siwon-L Aug 23, 2026
8e11843
chore: ignore CLAUDE.md
Siwon-L Aug 23, 2026
142bc1b
feat: handle invitation link on Ledger screen after login
Siwon-L Aug 23, 2026
706d0c2
fix: show loading indicator instead of resizing agency list
Siwon-L Aug 23, 2026
bece253
refactor: move Config to Shared/Utility module
Siwon-L Aug 23, 2026
8c08142
chore: ignore .claude directory
Siwon-L Aug 29, 2026
2a960cc
feat: share invite link from member tab
Siwon-L Aug 29, 2026
8bcb82c
fix: call addChild/didMove for embedded view controllers
Siwon-L Aug 29, 2026
e0b48dc
test: add MockAgencyRepository and MockUserRepository
Siwon-L Aug 30, 2026
162c621
test: add unit tests for ConfirmCertificateCodeUseCase
Siwon-L Aug 30, 2026
271c31d
fix: resolve newly joined agency from refetched list
Siwon-L Aug 31, 2026
bb19f5a
test: rename ConfirmCertificateCode tests and cover refetch after join
Siwon-L Aug 31, 2026
e30ac93
refactor: distinguish widget deep link from invitation link
Siwon-L Aug 31, 2026
16d9702
fix: validate invitation universal link path and query params
Siwon-L Aug 31, 2026
a63a582
fix: build invite link from web host instead of API base
Siwon-L Aug 31, 2026
36fb5dc
fix: always load agencies on ledger entry with invite fallback
Siwon-L Aug 31, 2026
b4e74cf
fix: show invite link button for non-member roles
Siwon-L Aug 31, 2026
1132ee2
feat: personalize invitation share message with inviter and agency name
Siwon-L Sep 10, 2026
2aeb6ba
chore: log network responses as public and drop unused queue
Siwon-L Sep 10, 2026
117172b
chore: bump marketing version to 2.2.1
Siwon-L Sep 10, 2026
1fef2e6
chore: drop profile id suffix from provisioning specifier
Siwon-L Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,5 @@ fastlane/report.xml
fastlane/README.md
Projects/App/Resources/APIKey.xcconfig
graph.png
CLAUDE.md
.claude
6 changes: 3 additions & 3 deletions Projects/App/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ let project = Project(
),
settings: .settings(
base: .init()
.marketingVersion("2.2.0")
.marketingVersion("2.2.1")
.swiftVersion("5.7")
.currentProjectVersion("1")
.appleGenericVersioningSystem()
Expand Down Expand Up @@ -118,11 +118,11 @@ let project = Project(
configurations: [
.debug(name: "Debug", settings: [
"CODE_SIGN_IDENTITY": "Apple Development: Nayeon Gu (3CMPGMMD7L)",
"PROVISIONING_PROFILE_SPECIFIER": "match Development com.yapp.moneymong 1769511003"
"PROVISIONING_PROFILE_SPECIFIER": "match Development com.yapp.moneymong"
]),
.release(name: "Release", settings: [
"CODE_SIGN_IDENTITY": "Apple Distribution: Nayeon Gu (H5G7RFWFSQ)",
"PROVISIONING_PROFILE_SPECIFIER": "match AppStore com.yapp.moneymong 1769511386"
"PROVISIONING_PROFILE_SPECIFIER": "match AppStore com.yapp.moneymong"
])
]
),
Expand Down
5 changes: 5 additions & 0 deletions Projects/App/Resources/App.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:dev.moneymong.site</string>
<string>applinks:prod.moneymong.site</string>
</array>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
Expand Down
35 changes: 34 additions & 1 deletion Projects/App/Sources/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Repository
import SignFeature
import User
import UserInterface
import Utility

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let localStorage = LocalStorage()
Expand All @@ -44,17 +45,32 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
appCoordinator?.start(animated: false)

self.scene(scene, openURLContexts: connectionOptions.urlContexts)

if let userActivity = connectionOptions.userActivities.first(
where: { $0.activityType == NSUserActivityTypeBrowsingWeb }
) {
handleUniversalLink(from: userActivity)
}
}

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }

if url.absoluteString.contains("widget://") {
DeepLinkManager.setDestination(url.absoluteString, agencyID: localStorage.selectedAgency)
let destination = url.absoluteString.replacingOccurrences(of: "widget://", with: "")
guard let agencyID = localStorage.selectedAgency else { return }
DeepLinkManager.setQuery([
"destination": destination,
"agencyID": agencyID
], notiName: .widgetLink)
} else {
KakaoAuthManager.shared.openURL(url)
}
}

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
handleUniversalLink(from: userActivity)
}

func sceneDidDisconnect(_ scene: UIScene) {}

Expand All @@ -65,6 +81,23 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func sceneWillEnterForeground(_ scene: UIScene) {}

func sceneDidEnterBackground(_ scene: UIScene) {}

private func handleUniversalLink(from userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL,
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
url.path == Config.invitationPath else { return }

let queryItems = components.queryItems ?? []
guard let code = queryItems.first(where: { $0.name == "code" })?.value,
let agencyID = queryItems.first(where: { $0.name == "agencyID" })?.value,
!code.isEmpty, !agencyID.isEmpty else { return }

DeepLinkManager.setQuery(
["code": code, "agencyID": agencyID],
notiName: .invitationLink
)
}
}

extension SceneDelegate {
Expand Down
6 changes: 2 additions & 4 deletions Projects/Core/MMNetwork/Sources/NetworkLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import OSLog
import Alamofire

final class NetworkLogger: EventMonitor {
let queue = DispatchQueue(label: "NetworkLogger")

private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "NetworkLogger")

func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) where Value : Sendable {
Expand Down Expand Up @@ -34,9 +32,9 @@ final class NetworkLogger: EventMonitor {
log.append("\(response.data?.toPrettyPrintedString ?? "None")")

if (200..<300) ~= statusCode {
logger.log("\(log)")
logger.log("\(log, privacy: .public)")
} else {
logger.error("\(log)")
logger.error("\(log, privacy: .public)")

}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

import MMNetworkInterface
import Utility

enum AgencyAPI {
case create(param: AgencyCreateRequestDTO) // 소속생성
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

import MMNetworkInterface
import Utility

import Alamofire

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

import MMNetworkInterface
import Utility

enum SignAPI {
case sign(SignRequestDTO)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

import MMNetworkInterface
import Utility

import Alamofire

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

import MMNetworkInterface
import Utility

enum UserAPI {
case user // 내정보 조회
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

import MMNetworkInterface
import Utility

enum VersionAPI {
case version(VersionRequestDTO) // 버전조회
Expand Down
7 changes: 0 additions & 7 deletions Projects/Core/Repository/Sources/Common/Config.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import BaseDomain
// 소속가입시, 초대코드가 맞는지 확인한다
public protocol ConfirmCertificateCodeUseCaseInterface {
func execute(code: [String]) async throws -> Agency?
func execute(code: String, agencyID: Int) async throws -> Agency?
}
3 changes: 2 additions & 1 deletion Projects/Domain/Agency/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ let project = Project(
sources: ["Tests/**"],
dependencies: [
.target(name: "Agency"),
.target(name: "AgencyTesting")
.target(name: "AgencyTesting"),
.project(target: "BaseDomainTesting", path: .relativeToRoot("Projects/Domain/BaseDomain")),
]
),
Target(
Expand Down
17 changes: 17 additions & 0 deletions Projects/Domain/Agency/Sources/ConfirmCertificateCodeUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,21 @@ public struct ConfirmCertificateCodeUseCase: ConfirmCertificateCodeUseCaseInterf
}
return nil
}

public func execute(code: String, agencyID: Int) async throws -> Agency? {
let agencies = try await agencyRepo.fetchMyAgency()
if let agency = agencies.first(where: { $0.id == agencyID }) {
// 이미 소속에 가입되어 있는 경우
userRepo.updateSelectedAgency(id: agencyID)
return agency
}

// 소속에 가입되어 있지 않은 경우
let response = try await agencyRepo.certificateCode(code: code)
guard response.certified else { return nil }

userRepo.updateSelectedAgency(id: response.agencyId)
// 방금 가입했으므로 가입 전 조회한 `agencies`(stale)가 아니라 재조회한 목록에서 찾는다.
return try await agencyRepo.fetchMyAgency().first { $0.id == response.agencyId }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ public struct MockConfirmCertificateCodeUseCase: ConfirmCertificateCodeUseCaseIn
public init() {}

public func execute(code: [String]) async throws -> Agency? { return nil }
public func execute(code: String, agencyID: Int) async throws -> Agency? {
return nil
}
}
120 changes: 120 additions & 0 deletions Projects/Domain/Agency/Tests/ConfirmCertificateCodeUseCaseTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import XCTest
@testable import Agency
@testable import BaseDomain
@testable import BaseDomainTesting

final class ConfirmCertificateCodeUseCaseTests: XCTestCase {
var sut: ConfirmCertificateCodeUseCase!
var mockAgencyRepo: MockAgencyRepository!
var mockUserRepo: MockUserRepository!

override func setUpWithError() throws {
mockAgencyRepo = MockAgencyRepository()
mockUserRepo = MockUserRepository()
sut = ConfirmCertificateCodeUseCase(agencyRepo: mockAgencyRepo, userRepo: mockUserRepo)
}

override func tearDownWithError() throws {
mockAgencyRepo = nil
mockUserRepo = nil
sut = nil
}

func test_execute_코드배열로_호출_시_인증에_성공하면_소속을_저장하고_해당_Agency를_반환한다() async {
// Arrange
let agency = Agency(id: 1, name: "몽테스트", count: 1)
mockAgencyRepo.returnValue.certificateCode = CertificationResult(certified: true, agencyId: agency.id)
mockAgencyRepo.returnValue.fetchMyAgency = [agency]

do {
// Act
let output = try await sut.execute(code: ["1", "2", "3", "4"])

// Assert
XCTAssertEqual(output, agency)
XCTAssertEqual(mockAgencyRepo.inputValue.certificateCode, "1234")
XCTAssertEqual(mockUserRepo.inputValue.updateSelectedAgency, agency.id)
XCTAssertEqual(mockUserRepo.callCount.updateSelectedAgency, 1)
} catch {
// Assert
XCTFail()
}
}

func test_execute_코드배열로_호출_시_인증에_실패하면_nil을_반환한다() async {
// Arrange
mockAgencyRepo.returnValue.certificateCode = CertificationResult(certified: false, agencyId: 0)

do {
// Act
let output = try await sut.execute(code: ["1", "2", "3", "4"])

// Assert
XCTAssertNil(output)
XCTAssertEqual(mockUserRepo.callCount.updateSelectedAgency, 0)
} catch {
// Assert
XCTFail()
}
}

func test_execute_코드와_소속ID로_호출_시_이미_가입된_소속이면_인증코드_확인없이_소속을_저장하고_해당_Agency를_반환한다() async {
// Arrange
let agency = Agency(id: 1, name: "몽테스트", count: 1)
mockAgencyRepo.returnValue.fetchMyAgency = [agency]

do {
// Act
let output = try await sut.execute(code: "1234", agencyID: agency.id)

// Assert
XCTAssertEqual(output, agency)
XCTAssertEqual(mockAgencyRepo.callCount.certificateCode, 0)
XCTAssertEqual(mockUserRepo.inputValue.updateSelectedAgency, agency.id)
} catch {
// Assert
XCTFail()
}
}

func test_execute_코드와_소속ID로_호출_시_가입되지_않은_소속이고_인증에_성공하면_재조회한_목록의_Agency를_반환한다() async {
// Arrange
let agency = Agency(id: 2, name: "몽테스트", count: 1)
// 가입 전 조회에는 해당 소속이 없고, 인증(가입) 후 재조회에서야 등장한다.
mockAgencyRepo.returnValue.fetchMyAgencySequence = [[], [agency]]
mockAgencyRepo.returnValue.certificateCode = CertificationResult(certified: true, agencyId: agency.id)

do {
// Act
let output = try await sut.execute(code: "1234", agencyID: 999)

// Assert
XCTAssertEqual(output, agency)
XCTAssertEqual(mockAgencyRepo.callCount.certificateCode, 1)
XCTAssertEqual(mockAgencyRepo.callCount.fetchMyAgency, 2)
XCTAssertEqual(mockUserRepo.inputValue.updateSelectedAgency, agency.id)
} catch {
// Assert
XCTFail()
}
}

func test_execute_코드와_소속ID로_호출_시_가입되지_않은_소속이고_인증에_실패하면_nil을_반환한다() async {
// Arrange
mockAgencyRepo.returnValue.fetchMyAgency = []
mockAgencyRepo.returnValue.certificateCode = CertificationResult(certified: false, agencyId: 0)

do {
// Act
let output = try await sut.execute(code: "1234", agencyID: 999)

// Assert
XCTAssertNil(output)
XCTAssertEqual(mockAgencyRepo.callCount.fetchMyAgency, 1)
XCTAssertEqual(mockUserRepo.callCount.updateSelectedAgency, 0)
} catch {
// Assert
XCTFail()
}
}
}
Loading