From 72edf278fbba21d1ea11f060e290e021d9e965c7 Mon Sep 17 00:00:00 2001 From: snughnu Date: Wed, 4 Feb 2026 11:01:01 +0900 Subject: [PATCH 1/4] =?UTF-8?q?test:=20LanguageGame=20didPerformAction=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=B8=A1=EC=A0=95=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GameCore/Models/Games/LanguageGame.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/SoloDeveloperTraining/SoloDeveloperTraining/GameCore/Models/Games/LanguageGame.swift b/SoloDeveloperTraining/SoloDeveloperTraining/GameCore/Models/Games/LanguageGame.swift index 01d1d6fa..354f5ba2 100644 --- a/SoloDeveloperTraining/SoloDeveloperTraining/GameCore/Models/Games/LanguageGame.swift +++ b/SoloDeveloperTraining/SoloDeveloperTraining/GameCore/Models/Games/LanguageGame.swift @@ -109,7 +109,25 @@ final class LanguageGame: Game { buffSystem.resume() } + // ===== πŸ“Š μΈ‘μ •μš© static λ³€μˆ˜ ===== + #if DEBUG + private static var callCount = 0 + #endif + func didPerformAction(_ input: LanguageType) async -> Int { + // ===== πŸ“Š μΈ‘μ • μ½”λ“œ μ‹œμž‘ ===== + #if DEBUG + Self.callCount += 1 + let currentCall = Self.callCount + let startTime = CFAbsoluteTimeGetCurrent() + var recordTime: Double = 0 + defer { + let totalElapsed = (CFAbsoluteTimeGetCurrent() - startTime) * 1000 + print("⏱️ [BEFORE #\(currentCall)] Total: \(String(format: "%.2f", totalElapsed))ms | Record: \(String(format: "%.2f", recordTime))ms") + } + #endif + // ===== πŸ“Š μΈ‘μ • μ½”λ“œ 끝 ===== + // Taskκ°€ μ·¨μ†Œλ˜μ—ˆμœΌλ©΄ μ¦‰μ‹œ μ’…λ£Œ guard !Task.isCancelled else { return 0 } @@ -133,17 +151,41 @@ final class LanguageGame: Game { ) if isSuccess { user.wallet.addGold(gainGold) + + // ===== πŸ“Š Record μ‹œκ°„ μΈ‘μ • μ‹œμž‘ ===== + #if DEBUG + let recordStart = CFAbsoluteTimeGetCurrent() + #endif + /// μ •λ‹΅ 횟수 기둝 user.record.record(.languageCorrect) /// λˆ„μ  μž¬μ‚° μ—…λ°μ΄νŠΈ user.record.record(.earnMoney(gainGold)) + + #if DEBUG + recordTime = (CFAbsoluteTimeGetCurrent() - recordStart) * 1000 + #endif + // ===== πŸ“Š Record μ‹œκ°„ μΈ‘μ • 끝 ===== + // μž¬ν™” νšλ“ μ‹œ 캐릭터 μ›ƒκ²Œ λ§Œλ“€κΈ° animationSystem?.playSmile() return gainGold } user.wallet.spendGold(Int(Double(gainGold) * Policy.Game.Language.incorrectGoldLossMultiplier)) + + // ===== πŸ“Š Record μ‹œκ°„ μΈ‘μ • μ‹œμž‘ (μ˜€λ‹΅ μΌ€μ΄μŠ€) ===== + #if DEBUG + let recordStart = CFAbsoluteTimeGetCurrent() + #endif + /// μ˜€λ‹΅ 횟수 기둝 user.record.record(.languageIncorrect) + + #if DEBUG + recordTime = (CFAbsoluteTimeGetCurrent() - recordStart) * 1000 + #endif + // ===== πŸ“Š Record μ‹œκ°„ μΈ‘μ • 끝 ===== + return Int(Double(gainGold) * Policy.Game.Language.incorrectGoldLossMultiplier) * -1 } From 697e14c71071b7f00b4a2aa4b06085a81086c1ca Mon Sep 17 00:00:00 2001 From: snughnu Date: Wed, 4 Feb 2026 16:59:10 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test:=20MissionSystem=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MissionSystemTests.swift | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 SoloDeveloperTraining/SoloDeveloperTrainingTests/MissionSystemTests.swift diff --git a/SoloDeveloperTraining/SoloDeveloperTrainingTests/MissionSystemTests.swift b/SoloDeveloperTraining/SoloDeveloperTrainingTests/MissionSystemTests.swift new file mode 100644 index 00000000..4e99a352 --- /dev/null +++ b/SoloDeveloperTraining/SoloDeveloperTrainingTests/MissionSystemTests.swift @@ -0,0 +1,130 @@ +// +// MissionSystemTests.swift +// SoloDeveloperTraining +// +// Created by κΉ€μ„±ν›ˆ on 2/4/26. +// + +import Testing +import Foundation +@testable import SoloDeveloperTraining + +@MainActor +struct MissionSystemTests { + + @Test("κΈ°λ³Έ λ―Έμ…˜ μ—…λ°μ΄νŠΈ λ™μž‘ 검증") + func testBasicMissionUpdate() async throws { + // Given: λͺ©ν‘œ 10인 μ–Έμ–΄ λ―Έμ…˜ 생성 + let mission = Mission( + id: 1, + type: .languageMatch(.bronze), + title: "μ–Έμ–΄ 10회 성곡", + description: "μ–Έμ–΄ κ²Œμž„ 10회 μ„±κ³΅ν•˜κΈ°", + targetValue: 10, + updateCondition: { $0.languageCorrectCount }, + reward: Cost(gold: 100) + ) + let missionSystem = MissionSystem(missions: [mission]) + let record = Record(missionSystem: missionSystem) + + // When: 10번 μ •λ‹΅ 기둝 + for _ in 1...10 { + record.record(.languageCorrect) + } + + // 비동기 처리 μ™„λ£Œ λŒ€κΈ° + try await Task.sleep(nanoseconds: 100_000_000) // 0.1초 + + // Then: λ―Έμ…˜ μ™„λ£Œλ˜μ–΄μ•Ό 함 + #expect(mission.currentValue == 10, "λ―Έμ…˜ 값이 10이어야 함") + #expect(mission.state == .claimable, "λ―Έμ…˜μ΄ μ™„λ£Œ μƒνƒœμ—¬μ•Ό 함") + #expect(missionSystem.hasCompletedMission == true, "μ™„λ£Œλœ λ―Έμ…˜μ΄ μžˆμ–΄μ•Ό 함") + } + + @Test("연속 μ—…λ°μ΄νŠΈ μ‹œ λˆ„λ½ λ°©μ§€ - 100회 연속 기둝") + func testConsecutiveUpdateGuarantee() async throws { + // Given: λͺ©ν‘œ 100인 μ–Έμ–΄ λ―Έμ…˜ + let mission = Mission( + id: 2, + type: .languageMatch(.silver), + title: "μ–Έμ–΄ 100회 성곡", + description: "μ–Έμ–΄ κ²Œμž„ 100회 μ„±κ³΅ν•˜κΈ°", + targetValue: 100, + updateCondition: { $0.languageCorrectCount }, + reward: Cost(gold: 1000) + ) + let missionSystem = MissionSystem(missions: [mission]) + let record = Record(missionSystem: missionSystem) + + // When: 100번 λΉ λ₯΄κ²Œ 연속 기둝 + for _ in 1...100 { + record.record(.languageCorrect) + } + + // 비동기 처리 μ™„λ£Œ λŒ€κΈ° + try await Task.sleep(nanoseconds: 100_000_000) // 0.1초 + + // Then: μ •ν™•νžˆ 100이어야 함 (λˆ„λ½ μ—†μŒ) + #expect(record.languageCorrectCount == 100, "λ ˆμ½”λ“œκ°€ μ •ν™•νžˆ 100이어야 함") + #expect(mission.currentValue == 100, "λ―Έμ…˜ 값이 μ •ν™•νžˆ 100이어야 함") + #expect(mission.state == .claimable, "λ―Έμ…˜μ΄ μ™„λ£Œλ˜μ–΄μ•Ό 함") + } + + @Test("μ—¬λŸ¬ λ―Έμ…˜ λ™μ‹œ μ—…λ°μ΄νŠΈ μ‹œ λˆ„λ½ λ°©μ§€ - 30개 λ―Έμ…˜") + func testMultipleMissionsUpdateWithoutLoss() async throws { + // Given: 30개의 λ―Έμ…˜ 생성 + let missions = (1...30).map { idx in + Mission( + id: idx, + type: .languageMatch(.bronze), + title: "λ―Έμ…˜ \(idx)", + description: "ν…ŒμŠ€νŠΈ λ―Έμ…˜", + targetValue: 1, + updateCondition: { $0.languageCorrectCount }, + reward: Cost(gold: 100) + ) + } + let missionSystem = MissionSystem(missions: missions) + let record = Record(missionSystem: missionSystem) + + // When: 1번만 기둝 + record.record(.languageCorrect) + + // 비동기 처리 μ™„λ£Œ λŒ€κΈ° + try await Task.sleep(nanoseconds: 100_000_000) // 0.1초 + + // Then: λͺ¨λ“  λ―Έμ…˜μ΄ μ™„λ£Œλ˜μ–΄μ•Ό 함 (λˆ„λ½ μ—†μŒ) + let completedCount = missions.filter { $0.state == .claimable }.count + #expect(completedCount == 30, "30개 λ―Έμ…˜ λͺ¨λ‘ μ™„λ£Œλ˜μ–΄μ•Ό 함 - λˆ„λ½ μ—†μŒ") + #expect(missionSystem.hasCompletedMission == true, "μ™„λ£Œ ν”Œλž˜κ·Έκ°€ trueμ—¬μ•Ό 함") + } + + @Test("earnMoney 이벀트둜 λˆ„μ  κ³¨λ“œ λ―Έμ…˜ μ—…λ°μ΄νŠΈ") + func testEarnMoneyEventMissionUpdate() async throws { + // Given: λˆ„μ  κ³¨λ“œ λ―Έμ…˜ + let mission = Mission( + id: 100, + type: .tap(.gold), + title: "1000κ³¨λ“œ νšλ“", + description: "λˆ„μ  1000κ³¨λ“œ 벌기", + targetValue: 1000, + updateCondition: { $0.totalEarnedMoney }, + reward: Cost(gold: 500, diamond: 10) + ) + let missionSystem = MissionSystem(missions: [mission]) + let record = Record(missionSystem: missionSystem) + + // When: μ—¬λŸ¬ 번 κ³¨λ“œ νšλ“ + record.record(.earnMoney(300)) + record.record(.earnMoney(400)) + record.record(.earnMoney(300)) + + // 비동기 처리 μ™„λ£Œ λŒ€κΈ° + try await Task.sleep(nanoseconds: 100_000_000) // 0.1초 + + // Then: μ •ν™•νžˆ 1000이어야 함 + #expect(record.totalEarnedMoney == 1000, "총 νšλ“ κ³¨λ“œκ°€ 1000이어야 함") + #expect(mission.currentValue == 1000, "λ―Έμ…˜ 값이 1000이어야 함") + #expect(mission.state == .claimable, "λ―Έμ…˜μ΄ μ™„λ£Œλ˜μ–΄μ•Ό 함") + } +} From 7de237d0c04a88d22eb6457c5610b9ca3b97cf5c Mon Sep 17 00:00:00 2001 From: snughnu Date: Wed, 4 Feb 2026 17:40:42 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20MissionSystem=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=83=81=ED=83=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/Systems/MissionSystem.swift | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/SoloDeveloperTraining/SoloDeveloperTraining/GameCore/Models/Systems/MissionSystem.swift b/SoloDeveloperTraining/SoloDeveloperTraining/GameCore/Models/Systems/MissionSystem.swift index 41e1749d..1ba0c3c3 100644 --- a/SoloDeveloperTraining/SoloDeveloperTraining/GameCore/Models/Systems/MissionSystem.swift +++ b/SoloDeveloperTraining/SoloDeveloperTraining/GameCore/Models/Systems/MissionSystem.swift @@ -29,13 +29,13 @@ final class MissionSystem { } /// 기둝을 ν†΅ν•˜μ—¬ ν˜„μž¬ λ―Έμ…˜μ˜ μƒνƒœλ“€μ„ μ—…λ°μ΄νŠΈ ν•©λ‹ˆλ‹€. + /// - Note: λΉ„λ™κΈ°λ‘œ μ²˜λ¦¬λ˜μ–΄ μ¦‰μ‹œ λ°˜ν™˜ + /// - Parameter record: μ—…λ°μ΄νŠΈν•  Record func updateCompletedMissions(record: Record) { - missions - .filter { $0.state == .inProgress } - .forEach { $0.update(record: record) } - - sortMissions() - checkHasCompletedMission() + // 메인 큐에 λΉ„λ™κΈ°λ‘œ μΆ”κ°€ (μ¦‰μ‹œ λ°˜ν™˜, μˆœμ„œ 보μž₯, λˆ„λ½ μ—†μŒ) + DispatchQueue.main.async { [weak self] in + self?.performMissionUpdate(record: record) + } } func claimMissionReward(mission: Mission, wallet: Wallet) { @@ -46,12 +46,25 @@ final class MissionSystem { sortMissions() checkHasCompletedMission() } +} + +// MARK: - Helper +private extension MissionSystem { + /// μ‹€μ œ λ―Έμ…˜ μ—…λ°μ΄νŠΈ μˆ˜ν–‰ (메인 μŠ€λ ˆλ“œ, 비동기) + func performMissionUpdate(record: Record) { + missions + .filter { $0.state == .inProgress } + .forEach { $0.update(record: record) } + + sortMissions() + checkHasCompletedMission() + } - private func checkHasCompletedMission() { + func checkHasCompletedMission() { hasCompletedMission = missions.contains { $0.state == .claimable } } - private func sortMissions() { + func sortMissions() { missions.sort { $0.state.rawValue < $1.state.rawValue } } } From 2e7cdc19a409c7e4e2de5735c945882a8191504a Mon Sep 17 00:00:00 2001 From: snughnu Date: Wed, 4 Feb 2026 17:43:01 +0900 Subject: [PATCH 4/4] =?UTF-8?q?remove:=20LanguageGame=20=EC=B8=A1=EC=A0=95?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GameCore/Models/Games/LanguageGame.swift | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/SoloDeveloperTraining/SoloDeveloperTraining/GameCore/Models/Games/LanguageGame.swift b/SoloDeveloperTraining/SoloDeveloperTraining/GameCore/Models/Games/LanguageGame.swift index 354f5ba2..7e4481f4 100644 --- a/SoloDeveloperTraining/SoloDeveloperTraining/GameCore/Models/Games/LanguageGame.swift +++ b/SoloDeveloperTraining/SoloDeveloperTraining/GameCore/Models/Games/LanguageGame.swift @@ -109,25 +109,7 @@ final class LanguageGame: Game { buffSystem.resume() } - // ===== πŸ“Š μΈ‘μ •μš© static λ³€μˆ˜ ===== - #if DEBUG - private static var callCount = 0 - #endif - func didPerformAction(_ input: LanguageType) async -> Int { - // ===== πŸ“Š μΈ‘μ • μ½”λ“œ μ‹œμž‘ ===== - #if DEBUG - Self.callCount += 1 - let currentCall = Self.callCount - let startTime = CFAbsoluteTimeGetCurrent() - var recordTime: Double = 0 - defer { - let totalElapsed = (CFAbsoluteTimeGetCurrent() - startTime) * 1000 - print("⏱️ [BEFORE #\(currentCall)] Total: \(String(format: "%.2f", totalElapsed))ms | Record: \(String(format: "%.2f", recordTime))ms") - } - #endif - // ===== πŸ“Š μΈ‘μ • μ½”λ“œ 끝 ===== - // Taskκ°€ μ·¨μ†Œλ˜μ—ˆμœΌλ©΄ μ¦‰μ‹œ μ’…λ£Œ guard !Task.isCancelled else { return 0 } @@ -152,40 +134,20 @@ final class LanguageGame: Game { if isSuccess { user.wallet.addGold(gainGold) - // ===== πŸ“Š Record μ‹œκ°„ μΈ‘μ • μ‹œμž‘ ===== - #if DEBUG - let recordStart = CFAbsoluteTimeGetCurrent() - #endif - /// μ •λ‹΅ 횟수 기둝 user.record.record(.languageCorrect) /// λˆ„μ  μž¬μ‚° μ—…λ°μ΄νŠΈ user.record.record(.earnMoney(gainGold)) - #if DEBUG - recordTime = (CFAbsoluteTimeGetCurrent() - recordStart) * 1000 - #endif - // ===== πŸ“Š Record μ‹œκ°„ μΈ‘μ • 끝 ===== - // μž¬ν™” νšλ“ μ‹œ 캐릭터 μ›ƒκ²Œ λ§Œλ“€κΈ° animationSystem?.playSmile() return gainGold } user.wallet.spendGold(Int(Double(gainGold) * Policy.Game.Language.incorrectGoldLossMultiplier)) - // ===== πŸ“Š Record μ‹œκ°„ μΈ‘μ • μ‹œμž‘ (μ˜€λ‹΅ μΌ€μ΄μŠ€) ===== - #if DEBUG - let recordStart = CFAbsoluteTimeGetCurrent() - #endif - /// μ˜€λ‹΅ 횟수 기둝 user.record.record(.languageIncorrect) - #if DEBUG - recordTime = (CFAbsoluteTimeGetCurrent() - recordStart) * 1000 - #endif - // ===== πŸ“Š Record μ‹œκ°„ μΈ‘μ • 끝 ===== - return Int(Double(gainGold) * Policy.Game.Language.incorrectGoldLossMultiplier) * -1 }