diff --git a/DanDan/DanDan.xcodeproj/project.pbxproj b/DanDan/DanDan.xcodeproj/project.pbxproj index 066e6b21..a2f2abd4 100644 --- a/DanDan/DanDan.xcodeproj/project.pbxproj +++ b/DanDan/DanDan.xcodeproj/project.pbxproj @@ -6,8 +6,19 @@ objectVersion = 77; objects = { +/* Begin PBXContainerItemProxy section */ + 690DD8DD2EADF478005EB019 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 690DD6E92EACB758005EB019 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 690DD6F02EACB758005EB019; + remoteInfo = DanDan; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ - 690DD6F12EACB758005EB019 /* DanDan.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DanDan.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 690DD6F12EACB758005EB019 /* DanDan.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; name = DanDan.app; path = "/Users/jay/Documents/GitHub/2025-C6-M4-DanDan/DanDan/build/Debug-iphoneos/DanDan.app"; sourceTree = ""; }; + 690DD8F22EADF755005EB019 /* DanDanTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = DanDanTests.xctest; path = "/Users/jay/Documents/GitHub/2025-C6-M4-DanDan/DanDan/build/Debug-iphoneos/DanDan.app/PlugIns/DanDanTests.xctest"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -16,6 +27,11 @@ path = DanDan; sourceTree = ""; }; + 690DD8EE2EADF6B1005EB019 /* DanDanTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = DanDanTests; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -26,6 +42,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 690DD8D62EADF478005EB019 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -33,16 +56,8 @@ isa = PBXGroup; children = ( 690DD6F32EACB758005EB019 /* DanDan */, - 690DD6F22EACB758005EB019 /* Products */, - ); - sourceTree = ""; - }; - 690DD6F22EACB758005EB019 /* Products */ = { - isa = PBXGroup; - children = ( - 690DD6F12EACB758005EB019 /* DanDan.app */, + 690DD8EE2EADF6B1005EB019 /* DanDanTests */, ); - name = Products; sourceTree = ""; }; /* End PBXGroup section */ @@ -70,6 +85,29 @@ productReference = 690DD6F12EACB758005EB019 /* DanDan.app */; productType = "com.apple.product-type.application"; }; + 690DD8D82EADF478005EB019 /* DanDanTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 690DD8DF2EADF478005EB019 /* Build configuration list for PBXNativeTarget "DanDanTests" */; + buildPhases = ( + 690DD8D52EADF478005EB019 /* Sources */, + 690DD8D62EADF478005EB019 /* Frameworks */, + 690DD8D72EADF478005EB019 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 690DD8DE2EADF478005EB019 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 690DD8EE2EADF6B1005EB019 /* DanDanTests */, + ); + name = DanDanTests; + packageProductDependencies = ( + ); + productName = DanDanTests; + productReference = 690DD8F22EADF755005EB019 /* DanDanTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -83,6 +121,10 @@ 690DD6F02EACB758005EB019 = { CreatedOnToolsVersion = 16.4; }; + 690DD8D82EADF478005EB019 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 690DD6F02EACB758005EB019; + }; }; }; buildConfigurationList = 690DD6EC2EACB758005EB019 /* Build configuration list for PBXProject "DanDan" */; @@ -94,12 +136,16 @@ ); mainGroup = 690DD6E82EACB758005EB019; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 690DD8E42EADF4F3005EB019 /* XCRemoteSwiftPackageReference "swift-async-algorithms" */, + ); preferredProjectObjectVersion = 77; - productRefGroup = 690DD6F22EACB758005EB019 /* Products */; + productRefGroup = 690DD6E82EACB758005EB019; projectDirPath = ""; projectRoot = ""; targets = ( 690DD6F02EACB758005EB019 /* DanDan */, + 690DD8D82EADF478005EB019 /* DanDanTests */, ); }; /* End PBXProject section */ @@ -112,6 +158,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 690DD8D72EADF478005EB019 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -122,8 +175,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 690DD8D52EADF478005EB019 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 690DD8DE2EADF478005EB019 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 690DD6F02EACB758005EB019 /* DanDan */; + targetProxy = 690DD8DD2EADF478005EB019 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 690DD6FA2EACB75A005EB019 /* Debug */ = { isa = XCBuildConfiguration; @@ -300,6 +368,42 @@ }; name = Release; }; + 690DD8E02EADF478005EB019 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = NBC28VW8Q2; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = C6.DanDanTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DanDan.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DanDan"; + }; + name = Debug; + }; + 690DD8E12EADF478005EB019 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = NBC28VW8Q2; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = C6.DanDanTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DanDan.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DanDan"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -321,7 +425,27 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 690DD8DF2EADF478005EB019 /* Build configuration list for PBXNativeTarget "DanDanTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 690DD8E02EADF478005EB019 /* Debug */, + 690DD8E12EADF478005EB019 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 690DD8E42EADF4F3005EB019 /* XCRemoteSwiftPackageReference "swift-async-algorithms" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-async-algorithms.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.4; + }; + }; +/* End XCRemoteSwiftPackageReference section */ }; rootObject = 690DD6E92EACB758005EB019 /* Project object */; } diff --git a/DanDan/DanDan/Sources/Models/ConquestPeriod.swift b/DanDan/DanDan/Sources/Models/ConquestPeriod.swift new file mode 100644 index 00000000..3f424847 --- /dev/null +++ b/DanDan/DanDan/Sources/Models/ConquestPeriod.swift @@ -0,0 +1,28 @@ +// +// ConquestPeriod.swift +// DanDan +// +// Created by Jay on 10/26/25. +// + +import Foundation + +struct ConquestPeriod { + let startDate: Date + let endDate: Date + + init(startDate: Date, durationInDays: Int = 7) { + self.startDate = startDate + + guard let calculatedEndDate = Calendar.current.date( + byAdding: .day, + value: durationInDays, + to: startDate + ) + else { + fatalError("ConquestPeriod 초기화 실패: startDate로부터 endDate 계산 불가") + } + + self.endDate = calculatedEndDate + } +} diff --git a/DanDan/DanDan/Sources/Models/Storage/.gitkeep b/DanDan/DanDan/Sources/Models/Storage/.gitkeep deleted file mode 100644 index 74fdd96b..00000000 --- a/DanDan/DanDan/Sources/Models/Storage/.gitkeep +++ /dev/null @@ -1,7 +0,0 @@ -// -// Untitled.swift -// DanDan -// -// Created by Jay on 10/25/25. -// - diff --git a/DanDan/DanDan/Sources/Utility/Extensions/ConquestPeriod+.swift b/DanDan/DanDan/Sources/Utility/Extensions/ConquestPeriod+.swift new file mode 100644 index 00000000..9e5cdce7 --- /dev/null +++ b/DanDan/DanDan/Sources/Utility/Extensions/ConquestPeriod+.swift @@ -0,0 +1,28 @@ +// +// ConquestPeriod+.swift +// DanDan +// +// Created by Jay on 10/26/25. +// + +import Foundation + +extension ConquestPeriod { + func isWithinPeriod(date: Date = Date()) -> Bool { + (startDate...endDate).contains(date) + } + + var hasEnded: Bool { + Date() > endDate + } + + func daysLeft(from date: Date = Date()) -> Int { + let remaining = Calendar.current.dateComponents( + [.day], + from: date, + to: endDate + ).day ?? 0 + + return max(0, remaining) + } +} diff --git a/DanDan/DanDanTests/ConquestPeriodTests.swift b/DanDan/DanDanTests/ConquestPeriodTests.swift new file mode 100644 index 00000000..99f2d134 --- /dev/null +++ b/DanDan/DanDanTests/ConquestPeriodTests.swift @@ -0,0 +1,71 @@ +// +// ConquestPeriodTests.swift +// DanDan +// +// Created by Jay on 10/26/25. +// + +import XCTest +import os +@testable import DanDan + +final class ConquestPeriodTests: XCTestCase { + private let logger = Logger(subsystem: "com.dandan.tests", category: "ConquestPeriod") + + func test_endDateCalculation() { + let start = Date(timeIntervalSince1970: 0) + let period = ConquestPeriod(startDate: start) + let expectedEnd = Calendar.current.date(byAdding: .day, value: 7, to: start) + + logger.info("🧪 endDateCalculation → start: \(start, privacy: .public), expectedEnd: \(expectedEnd ?? Date(), privacy: .public), actualEnd: \(period.endDate, privacy: .public)") + + XCTAssertEqual(period.endDate, expectedEnd) + } + + func test_isWithinPeriod() { + let start = Date() + let period = ConquestPeriod(startDate: start) + + let midDate = Calendar.current.date(byAdding: .day, value: 3, to: start)! + let afterDate = Calendar.current.date(byAdding: .day, value: 8, to: start)! + + logger.info("🧪 isWithinPeriod") + logger.info(" - start: \(period.isWithinPeriod(date: start), privacy: .public)") + logger.info(" - mid (3일 후): \(period.isWithinPeriod(date: midDate), privacy: .public)") + logger.info(" - end: \(period.isWithinPeriod(date: period.endDate), privacy: .public)") + logger.info(" - after (8일 후): \(period.isWithinPeriod(date: afterDate), privacy: .public)") + + XCTAssertTrue(period.isWithinPeriod(date: start)) + XCTAssertTrue(period.isWithinPeriod(date: midDate)) + XCTAssertTrue(period.isWithinPeriod(date: period.endDate)) + XCTAssertFalse(period.isWithinPeriod(date: afterDate)) + } + + func test_hasEnded() { + let start = Calendar.current.date(byAdding: .day, value: -10, to: Date())! + let period = ConquestPeriod(startDate: start) + + logger.info("🧪 hasEnded → start: \(start, privacy: .public), now: \(Date(), privacy: .public), hasEnded: \(period.hasEnded, privacy: .public)") + + XCTAssertTrue(period.hasEnded) + } + + func test_daysLeft() { + let start = Date() + let period = ConquestPeriod(startDate: start) + + let midDate = Calendar.current.date(byAdding: .day, value: 3, to: start)! + let overDate = Calendar.current.date(byAdding: .day, value: 10, to: start)! + + logger.info("🧪daysLeft") + logger.info(" - from start: \(period.daysLeft(from: start), privacy: .public)") + logger.info(" - from end: \(period.daysLeft(from: period.endDate), privacy: .public)") + logger.info(" - from mid (3일 후): \(period.daysLeft(from: midDate), privacy: .public)") + logger.info(" - from over (10일 후): \(period.daysLeft(from: overDate), privacy: .public)") + + XCTAssertEqual(period.daysLeft(from: start), 7) + XCTAssertEqual(period.daysLeft(from: period.endDate), 0) + XCTAssertEqual(period.daysLeft(from: midDate), 4) + XCTAssertEqual(period.daysLeft(from: overDate), 0) + } +}