From 9f46f301af5e896c289b53c6ebc268b4d0daf8ca Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 01:12:16 +0900 Subject: [PATCH 01/23] ActivityStat UseCase Test --- .../DummyClimbRecordRepositoryImpl.swift | 14 +- .../Dummy/DummyForecastRepositoryImpl.swift | 22 +- .../DummyTrackActivityRepositoryImpl.swift | 17 +- .../TrackActivity/ActivityStatUseCase.swift | 7 +- .../UseCases/ActivityStatUseCaseTests.swift | 193 ++++++++++++ .../FetchWeeklyForecastUseCaseTests.swift | 297 ++++++++++++++++++ .../GetActivityLogsUseCaseTests.swift | 224 +++++++++++++ .../GetAverageActivityStatsUseCaseTests.swift | 250 +++++++++++++++ .../UseCases/GetRecordStatsUseCaseTests.swift | 150 +++++++++ Project.swift | 17 + 10 files changed, 1181 insertions(+), 10 deletions(-) create mode 100644 DomainTests/UseCases/ActivityStatUseCaseTests.swift create mode 100644 DomainTests/UseCases/FetchWeeklyForecastUseCaseTests.swift create mode 100644 DomainTests/UseCases/GetActivityLogsUseCaseTests.swift create mode 100644 DomainTests/UseCases/GetAverageActivityStatsUseCaseTests.swift create mode 100644 DomainTests/UseCases/GetRecordStatsUseCaseTests.swift diff --git a/Data/Sources/Repositories/Dummy/DummyClimbRecordRepositoryImpl.swift b/Data/Sources/Repositories/Dummy/DummyClimbRecordRepositoryImpl.swift index 45d7ee7..73641d5 100644 --- a/Data/Sources/Repositories/Dummy/DummyClimbRecordRepositoryImpl.swift +++ b/Data/Sources/Repositories/Dummy/DummyClimbRecordRepositoryImpl.swift @@ -16,10 +16,20 @@ public final class DummyClimbRecordRepositoryImpl: ClimbRecordRepository { // Dummy data private var dummyClimbRecords = ClimbRecord.dummy - private init() {} + // Test properties + var mockRecords: [ClimbRecord] = [] + var shouldReturnError = false + + init() {} public func fetch(keyword: String, isOnlyBookmarked: Bool) -> AnyPublisher, Never> { - var records = dummyClimbRecords + if shouldReturnError { + return Just(.failure(NSError(domain: "Test", code: -1, userInfo: nil))) + .eraseToAnyPublisher() + } + + // Use mockRecords if available, otherwise use dummy data + var records = mockRecords.isEmpty ? dummyClimbRecords : mockRecords if !keyword.isEmpty { let ids = Mountain.dummy.filter { $0.name.contains(keyword)}.map { $0.id } diff --git a/Data/Sources/Repositories/Dummy/DummyForecastRepositoryImpl.swift b/Data/Sources/Repositories/Dummy/DummyForecastRepositoryImpl.swift index 6a8c59f..328363a 100644 --- a/Data/Sources/Repositories/Dummy/DummyForecastRepositoryImpl.swift +++ b/Data/Sources/Repositories/Dummy/DummyForecastRepositoryImpl.swift @@ -11,9 +11,29 @@ import Domain public final class DummyForecastRepositoryImpl: ForecastRepository { - public init() {} + // Test properties + var mockForecastItems: [ForecastItem] = [] + var shouldReturnError = false + var lastNx: Int? + var lastNy: Int? + + init() {} public func fetchShortTermForecast(nx: Int, ny: Int) -> AnyPublisher, Never> { + lastNx = nx + lastNy = ny + + if shouldReturnError { + return Just(.failure(NSError(domain: "Test", code: -1, userInfo: nil))) + .eraseToAnyPublisher() + } + + // Use mockForecastItems if available, otherwise generate random data + if !mockForecastItems.isEmpty { + return Just(.success(mockForecastItems)) + .eraseToAnyPublisher() + } + let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyyMMdd" diff --git a/Data/Sources/Repositories/Dummy/DummyTrackActivityRepositoryImpl.swift b/Data/Sources/Repositories/Dummy/DummyTrackActivityRepositoryImpl.swift index 69f7426..9aacb09 100644 --- a/Data/Sources/Repositories/Dummy/DummyTrackActivityRepositoryImpl.swift +++ b/Data/Sources/Repositories/Dummy/DummyTrackActivityRepositoryImpl.swift @@ -18,7 +18,11 @@ public final class DummyTrackActivityRepositoryImpl: TrackActivityRepository { private let dataUpdateSubject = PassthroughSubject() private var timer: Timer? - private init() {} + // Test properties + var mockLogs: [ActivityLog] = [] + var shouldReturnError = false + + init() {} public func requestAuthorization() -> AnyPublisher, Never> { return Just(.success(true)) @@ -38,6 +42,17 @@ public final class DummyTrackActivityRepositoryImpl: TrackActivityRepository { } public func getActivityLogs() -> AnyPublisher, Never> { + if shouldReturnError { + return Just(.failure(NSError(domain: "Test", code: -1, userInfo: nil))) + .eraseToAnyPublisher() + } + + // Use mockLogs if available, otherwise generate random data + if !mockLogs.isEmpty { + return Just(.success(mockLogs)) + .eraseToAnyPublisher() + } + guard let startDate = self.startDate else { return Just(.failure(NSError(domain: "No tracking session found", code: -1))) .eraseToAnyPublisher() diff --git a/Domain/Sources/UseCases/TrackActivity/ActivityStatUseCase.swift b/Domain/Sources/UseCases/TrackActivity/ActivityStatUseCase.swift index 9996d81..03358cd 100644 --- a/Domain/Sources/UseCases/TrackActivity/ActivityStatUseCase.swift +++ b/Domain/Sources/UseCases/TrackActivity/ActivityStatUseCase.swift @@ -56,13 +56,8 @@ public final class ActivityStatUseCaseImpl: ActivityStatUseCase { } } } - - let restMinutes = totalTimeMinutes - exerciseMinutes - // 로그가 초기값만 있는 경우 전체 시간을 운동 시간으로 계산 - if logsToProcess.isEmpty && totalTimeMinutes > 0 { - exerciseMinutes = totalTimeMinutes - } + let restMinutes = totalTimeMinutes - exerciseMinutes return ActivityStat( totalTimeMinutes: totalTimeMinutes, diff --git a/DomainTests/UseCases/ActivityStatUseCaseTests.swift b/DomainTests/UseCases/ActivityStatUseCaseTests.swift new file mode 100644 index 0000000..7ccabf1 --- /dev/null +++ b/DomainTests/UseCases/ActivityStatUseCaseTests.swift @@ -0,0 +1,193 @@ +// +// ActivityStatUseCaseTests.swift +// DomainTests +// +// Created by 김영훈 on 2025-11-28. +// + +import XCTest +@testable import Domain + +final class ActivityStatUseCaseTests: XCTestCase { + + var sut: ActivityStatUseCaseImpl! + + override func setUp() { + super.setUp() + sut = ActivityStatUseCaseImpl() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + // MARK: - Tests + + func test_execute_WithEmptyLogs_ReturnsZeroStats() { + // Given + let logs: [ActivityLog] = [] + + // When + let result = sut.execute(activityLogs: logs) + + // Then + XCTAssertEqual(result.totalTimeMinutes, 0) + XCTAssertEqual(result.totalDistance, 0) + XCTAssertEqual(result.totalSteps, 0) + XCTAssertNil(result.startTime) + XCTAssertNil(result.endTime) + XCTAssertEqual(result.exerciseMinutes, 0) + XCTAssertEqual(result.restMinutes, 0) + } + + func test_execute_WithSingleInitialLog_ReturnsZeroStats() { + // Given + let startDate = Date() + let logs = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0) + ] + + // When + let result = sut.execute(activityLogs: logs) + + // Then + XCTAssertEqual(result.totalTimeMinutes, 0) + XCTAssertEqual(result.totalDistance, 0) + XCTAssertEqual(result.totalSteps, 0) + XCTAssertEqual(result.startTime, startDate) + XCTAssertEqual(result.endTime, startDate) + XCTAssertEqual(result.exerciseMinutes, 0) + XCTAssertEqual(result.restMinutes, 0) + } + + func test_execute_WithInitialLogOnly_CountsAsRest() { + // Given + let startDate = Date() + let endDate = startDate.addingTimeInterval(30 * 60) // 30분 후 + let logs = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0), + ActivityLog(id: "2", time: endDate, step: 0, distance: 0) + ] + + // When + let result = sut.execute(activityLogs: logs) + + // Then + XCTAssertEqual(result.totalTimeMinutes, 30) + XCTAssertEqual(result.exerciseMinutes, 0, "모든 걸음이 0이면 운동 시간은 0") + XCTAssertEqual(result.restMinutes, 30, "모든 걸음이 0이면 전체 시간을 휴식 시간으로 계산") + } + + func test_execute_WithExerciseLogs_CalculatesCorrectExerciseTime() { + // Given + let startDate = Date() + let logs = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0), + ActivityLog(id: "2", time: startDate.addingTimeInterval(5 * 60), step: 150, distance: 100), // 5분 후, 150걸음 (운동) + ActivityLog(id: "3", time: startDate.addingTimeInterval(10 * 60), step: 200, distance: 150), // 10분 후, 200걸음 (운동) + ] + + // When + let result = sut.execute(activityLogs: logs) + + // Then + XCTAssertEqual(result.totalTimeMinutes, 10) + XCTAssertEqual(result.totalSteps, 350, "150 + 200 = 350") + XCTAssertEqual(result.totalDistance, 250, "100 + 150 = 250") + XCTAssertEqual(result.exerciseMinutes, 10, "5분당 100걸음 이상이므로 운동 시간") + XCTAssertEqual(result.restMinutes, 0) + } + + func test_execute_WithRestLogs_CalculatesCorrectRestTime() { + // Given + let startDate = Date() + let logs = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0), + ActivityLog(id: "2", time: startDate.addingTimeInterval(5 * 60), step: 50, distance: 30), // 5분 후, 50걸음 (휴식) + ActivityLog(id: "3", time: startDate.addingTimeInterval(10 * 60), step: 80, distance: 50), // 10분 후, 80걸음 (휴식) + ] + + // When + let result = sut.execute(activityLogs: logs) + + // Then + XCTAssertEqual(result.totalTimeMinutes, 10) + XCTAssertEqual(result.exerciseMinutes, 0, "5분당 100걸음 미만이므로 휴식 시간") + XCTAssertEqual(result.restMinutes, 10) + } + + func test_execute_WithMixedLogs_CalculatesMixedExerciseAndRest() { + // Given + let startDate = Date() + let logs = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0), + ActivityLog(id: "2", time: startDate.addingTimeInterval(5 * 60), step: 150, distance: 100), // 운동 + ActivityLog(id: "3", time: startDate.addingTimeInterval(10 * 60), step: 50, distance: 30), // 휴식 + ActivityLog(id: "4", time: startDate.addingTimeInterval(15 * 60), step: 200, distance: 150), // 운동 + ] + + // When + let result = sut.execute(activityLogs: logs) + + // Then + XCTAssertEqual(result.totalTimeMinutes, 15) + XCTAssertEqual(result.totalSteps, 400) + XCTAssertEqual(result.totalDistance, 280) + XCTAssertEqual(result.exerciseMinutes, 10, "5분(운동) + 5분(운동) = 10분") + XCTAssertEqual(result.restMinutes, 5, "5분(휴식)") + } + + func test_execute_WithLastLogLowStepsPerMinute_CountsAsRest() { + // Given + let startDate = Date() + let logs = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0), + ActivityLog(id: "2", time: startDate.addingTimeInterval(5 * 60), step: 150, distance: 100), // 운동 + ActivityLog(id: "3", time: startDate.addingTimeInterval(10 * 60), step: 50, distance: 30), // 마지막 로그, 분당 10보 (휴식) + ] + + // When + let result = sut.execute(activityLogs: logs) + + // Then + XCTAssertEqual(result.totalTimeMinutes, 10) + XCTAssertEqual(result.exerciseMinutes, 5, "첫 번째 구간만 운동") + XCTAssertEqual(result.restMinutes, 5, "마지막 로그가 분당 20보 미만이므로 휴식") + } + + func test_execute_WithLastLogHighStepsPerMinute_CountsAsExercise() { + // Given + let startDate = Date() + let logs = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0), + ActivityLog(id: "2", time: startDate.addingTimeInterval(5 * 60), step: 150, distance: 100), // 운동 + ActivityLog(id: "3", time: startDate.addingTimeInterval(10 * 60), step: 150, distance: 100), // 마지막 로그, 분당 30보 (운동) + ] + + // When + let result = sut.execute(activityLogs: logs) + + // Then + XCTAssertEqual(result.totalTimeMinutes, 10) + XCTAssertEqual(result.exerciseMinutes, 10, "모든 구간이 운동") + XCTAssertEqual(result.restMinutes, 0) + } + + func test_execute_ReturnsCorrectStartAndEndTime() { + // Given + let startDate = Date(timeIntervalSince1970: 1000000) + let endDate = Date(timeIntervalSince1970: 1001800) // 30분 후 + let logs = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0), + ActivityLog(id: "2", time: endDate, step: 150, distance: 100), + ] + + // When + let result = sut.execute(activityLogs: logs) + + // Then + XCTAssertEqual(result.startTime, startDate) + XCTAssertEqual(result.endTime, endDate) + } +} diff --git a/DomainTests/UseCases/FetchWeeklyForecastUseCaseTests.swift b/DomainTests/UseCases/FetchWeeklyForecastUseCaseTests.swift new file mode 100644 index 0000000..68597ad --- /dev/null +++ b/DomainTests/UseCases/FetchWeeklyForecastUseCaseTests.swift @@ -0,0 +1,297 @@ +// +// FetchWeeklyForecastUseCaseTests.swift +// DomainTests +// +// Created by 김영훈 on 2025-11-28. +// + +import XCTest +import Combine +@testable import Domain +@testable import Data + +final class FetchWeeklyForecastUseCaseTests: XCTestCase { + + var sut: FetchWeeklyForecastUseCaseImpl! + var dummyRepository: DummyForecastRepositoryImpl! + var cancellables: Set! + + override func setUp() { + super.setUp() + dummyRepository = DummyForecastRepositoryImpl() + sut = FetchWeeklyForecastUseCaseImpl(repository: dummyRepository) + cancellables = [] + } + + override func tearDown() { + cancellables = nil + sut = nil + dummyRepository = nil + super.tearDown() + } + + // MARK: - Tests + + func test_execute_WithEmptyForecastItems_ReturnsEmptyArray() { + // Given + dummyRepository.mockForecastItems = [] + let expectation = expectation(description: "Empty forecast items") + + // When + sut.execute(longitude: 126.9780, latitude: 37.5665) + .sink { result in + switch result { + case .success(let forecasts): + // Then + XCTAssertTrue(forecasts.isEmpty) + expectation.fulfill() + case .failure: + XCTFail("빈 예보 항목에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithSingleDayForecast_ReturnsProcessedForecast() { + // Given + let mockItems = [ + ForecastItem(date: "20251128", time: "0900", category: "TMP", value: "10"), + ForecastItem(date: "20251128", time: "1200", category: "TMP", value: "15"), + ForecastItem(date: "20251128", time: "1500", category: "TMP", value: "12"), + ForecastItem(date: "20251128", time: "0900", category: "POP", value: "30"), + ForecastItem(date: "20251128", time: "1200", category: "POP", value: "50"), + ForecastItem(date: "20251128", time: "0900", category: "PTY", value: "0"), + ] + dummyRepository.mockForecastItems = mockItems + let expectation = expectation(description: "Single day forecast") + + // When + sut.execute(longitude: 126.9780, latitude: 37.5665) + .sink { result in + switch result { + case .success(let forecasts): + // Then + XCTAssertEqual(forecasts.count, 1) + let forecast = forecasts[0] + XCTAssertEqual(forecast.minTemp, 10.0) + XCTAssertEqual(forecast.maxTemp, 15.0) + XCTAssertEqual(forecast.pop, 50) // 최대값 + XCTAssertEqual(forecast.pty, 0) + expectation.fulfill() + case .failure: + XCTFail("단일 날짜 예보에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithMultipleDaysForecast_ReturnsGroupedAndSortedForecasts() { + // Given + let mockItems = [ + // 11월 28일 + ForecastItem(date: "20251128", time: "0900", category: "TMP", value: "10"), + ForecastItem(date: "20251128", time: "1200", category: "TMP", value: "15"), + ForecastItem(date: "20251128", time: "0900", category: "POP", value: "30"), + ForecastItem(date: "20251128", time: "0900", category: "PTY", value: "0"), + // 11월 29일 + ForecastItem(date: "20251129", time: "0900", category: "TMP", value: "8"), + ForecastItem(date: "20251129", time: "1200", category: "TMP", value: "12"), + ForecastItem(date: "20251129", time: "0900", category: "POP", value: "60"), + ForecastItem(date: "20251129", time: "0900", category: "PTY", value: "1"), + // 11월 27일 (이전 날짜) + ForecastItem(date: "20251127", time: "0900", category: "TMP", value: "5"), + ForecastItem(date: "20251127", time: "1200", category: "TMP", value: "9"), + ForecastItem(date: "20251127", time: "0900", category: "POP", value: "20"), + ForecastItem(date: "20251127", time: "0900", category: "PTY", value: "0"), + ] + dummyRepository.mockForecastItems = mockItems + let expectation = expectation(description: "Multiple days forecast") + + // When + sut.execute(longitude: 126.9780, latitude: 37.5665) + .sink { result in + switch result { + case .success(let forecasts): + // Then + XCTAssertEqual(forecasts.count, 3) + + // 날짜순으로 정렬되어야 함 + XCTAssertLessThan(forecasts[0].date, forecasts[1].date) + XCTAssertLessThan(forecasts[1].date, forecasts[2].date) + + // 각 날짜의 데이터 확인 + let forecast27 = forecasts[0] + XCTAssertEqual(forecast27.minTemp, 5.0) + XCTAssertEqual(forecast27.maxTemp, 9.0) + XCTAssertEqual(forecast27.pop, 20) + XCTAssertEqual(forecast27.pty, 0) + + let forecast28 = forecasts[1] + XCTAssertEqual(forecast28.minTemp, 10.0) + XCTAssertEqual(forecast28.maxTemp, 15.0) + XCTAssertEqual(forecast28.pop, 30) + XCTAssertEqual(forecast28.pty, 0) + + let forecast29 = forecasts[2] + XCTAssertEqual(forecast29.minTemp, 8.0) + XCTAssertEqual(forecast29.maxTemp, 12.0) + XCTAssertEqual(forecast29.pop, 60) + XCTAssertEqual(forecast29.pty, 1) + + expectation.fulfill() + case .failure: + XCTFail("다중 날짜 예보에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithInvalidDateFormat_SkipsThatDate() { + // Given + let mockItems = [ + ForecastItem(date: "20251128", time: "0900", category: "TMP", value: "10"), + ForecastItem(date: "20251128", time: "1200", category: "TMP", value: "15"), + ForecastItem(date: "20251128", time: "0900", category: "POP", value: "30"), + ForecastItem(date: "20251128", time: "0900", category: "PTY", value: "0"), + // 잘못된 날짜 형식 + ForecastItem(date: "invalid", time: "0900", category: "TMP", value: "20"), + ] + dummyRepository.mockForecastItems = mockItems + let expectation = expectation(description: "Invalid date format") + + // When + sut.execute(longitude: 126.9780, latitude: 37.5665) + .sink { result in + switch result { + case .success(let forecasts): + // Then + // 잘못된 날짜는 제외되고 유효한 날짜만 반환됨 + XCTAssertEqual(forecasts.count, 1) + expectation.fulfill() + case .failure: + XCTFail("잘못된 날짜가 있어도 유효한 날짜는 처리되어야 함") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithNoTemperatureData_SkipsThatDate() { + // Given + let mockItems = [ + // 온도 데이터 없음 + ForecastItem(date: "20251128", time: "0900", category: "POP", value: "30"), + ForecastItem(date: "20251128", time: "0900", category: "PTY", value: "0"), + ] + dummyRepository.mockForecastItems = mockItems + let expectation = expectation(description: "No temperature data") + + // When + sut.execute(longitude: 126.9780, latitude: 37.5665) + .sink { result in + switch result { + case .success(let forecasts): + // Then + // 온도 데이터가 없으면 해당 날짜는 제외됨 + XCTAssertTrue(forecasts.isEmpty) + expectation.fulfill() + case .failure: + XCTFail("온도 데이터가 없어도 에러가 아님") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_ConvertsCoordinatesToGrid() { + // Given + dummyRepository.mockForecastItems = [] + let expectation = expectation(description: "Coordinate conversion") + + // 서울시청 좌표 + let longitude = 126.9780 + let latitude = 37.5665 + + // When + sut.execute(longitude: longitude, latitude: latitude) + .sink { result in + switch result { + case .success: + // Then + // 좌표가 격자 좌표로 변환되어 repository에 전달되었는지 확인 + XCTAssertNotNil(self.dummyRepository.lastNx) + XCTAssertNotNil(self.dummyRepository.lastNy) + expectation.fulfill() + case .failure: + XCTFail("좌표 변환에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithRepositoryError_ReturnsFailure() { + // Given + dummyRepository.shouldReturnError = true + let expectation = expectation(description: "Repository error") + + // When + sut.execute(longitude: 126.9780, latitude: 37.5665) + .sink { result in + switch result { + case .success: + XCTFail("에러가 발생해야 함") + case .failure(let error): + // Then + XCTAssertNotNil(error) + expectation.fulfill() + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_AggregatesMaxPOPAndPTY() { + // Given + let mockItems = [ + ForecastItem(date: "20251128", time: "0900", category: "TMP", value: "10"), + ForecastItem(date: "20251128", time: "0900", category: "POP", value: "30"), + ForecastItem(date: "20251128", time: "1200", category: "POP", value: "50"), + ForecastItem(date: "20251128", time: "1500", category: "POP", value: "20"), + ForecastItem(date: "20251128", time: "0900", category: "PTY", value: "0"), + ForecastItem(date: "20251128", time: "1200", category: "PTY", value: "1"), + ForecastItem(date: "20251128", time: "1500", category: "PTY", value: "2"), + ] + dummyRepository.mockForecastItems = mockItems + let expectation = expectation(description: "Max POP and PTY") + + // When + sut.execute(longitude: 126.9780, latitude: 37.5665) + .sink { result in + switch result { + case .success(let forecasts): + // Then + XCTAssertEqual(forecasts.count, 1) + let forecast = forecasts[0] + XCTAssertEqual(forecast.pop, 50, "최대 강수확률 50%") + XCTAssertEqual(forecast.pty, 2, "최대 강수형태 2") + expectation.fulfill() + case .failure: + XCTFail("집계에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } +} diff --git a/DomainTests/UseCases/GetActivityLogsUseCaseTests.swift b/DomainTests/UseCases/GetActivityLogsUseCaseTests.swift new file mode 100644 index 0000000..03ac8c2 --- /dev/null +++ b/DomainTests/UseCases/GetActivityLogsUseCaseTests.swift @@ -0,0 +1,224 @@ +// +// GetActivityLogsUseCaseTests.swift +// DomainTests +// +// Created by 김영훈 on 2025-11-28. +// + +import XCTest +import Combine +@testable import Domain +@testable import Data + +final class GetActivityLogsUseCaseTests: XCTestCase { + + var sut: GetActivityLogsUseCaseImpl! + var dummyRepository: DummyTrackActivityRepositoryImpl! + var cancellables: Set! + + override func setUp() { + super.setUp() + dummyRepository = DummyTrackActivityRepositoryImpl() + sut = GetActivityLogsUseCaseImpl(repository: dummyRepository) + cancellables = [] + } + + override func tearDown() { + cancellables = nil + sut = nil + dummyRepository = nil + super.tearDown() + } + + // MARK: - Tests + + func test_execute_WithEmptyLogs_ReturnsEmptyArray() { + // Given + dummyRepository.mockLogs = [] + let expectation = expectation(description: "Empty logs") + + // When + sut.execute() + .sink { result in + switch result { + case .success(let logs): + // Then + XCTAssertTrue(logs.isEmpty) + expectation.fulfill() + case .failure: + XCTFail("빈 로그에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithSingleLog_ReturnsSameLog() { + // Given + let log = ActivityLog(id: "1", time: Date(), step: 100, distance: 50) + dummyRepository.mockLogs = [log] + let expectation = expectation(description: "Single log") + + // When + sut.execute() + .sink { result in + switch result { + case .success(let logs): + // Then + XCTAssertEqual(logs.count, 1) + expectation.fulfill() + case .failure: + XCTFail("단일 로그에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithNormalLogs_AppliesSmoothing() { + // Given + let startDate = Date() + let logs = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0), + ActivityLog(id: "2", time: startDate.addingTimeInterval(5 * 60), step: 100, distance: 50), + ActivityLog(id: "3", time: startDate.addingTimeInterval(10 * 60), step: 200, distance: 100), + ActivityLog(id: "4", time: startDate.addingTimeInterval(15 * 60), step: 300, distance: 150), + ] + dummyRepository.mockLogs = logs + let expectation = expectation(description: "Normal logs") + + // When + sut.execute() + .sink { result in + switch result { + case .success(let smoothedLogs): + // Then + XCTAssertEqual(smoothedLogs.count, 4) + // 0이 아닌 데이터는 스무딩 적용됨 + XCTAssertTrue(smoothedLogs.allSatisfy { $0.step >= 0 && $0.distance >= 0 }) + expectation.fulfill() + case .failure: + XCTFail("정상 로그에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithSpikeLogs_AppliesCorrection() { + // Given + let startDate = Date() + let logs = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0), + ActivityLog(id: "2", time: startDate.addingTimeInterval(5 * 60), step: 100, distance: 50), + ActivityLog(id: "3", time: startDate.addingTimeInterval(10 * 60), step: 700, distance: 350), // 스파이크 + ActivityLog(id: "4", time: startDate.addingTimeInterval(15 * 60), step: 800, distance: 400), + ] + dummyRepository.mockLogs = logs + let expectation = expectation(description: "Spike logs") + + // When + sut.execute() + .sink { result in + switch result { + case .success(let correctedLogs): + // Then + XCTAssertEqual(correctedLogs.count, 4) + // 스파이크(500보 초과)가 보정되어야 함 + // 보정 후 스무딩이 적용되므로 결과값이 원본과 다름 + expectation.fulfill() + case .failure: + XCTFail("스파이크 로그에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithAllZeroLogs_ReturnsAllZeroLogs() { + // Given + let startDate = Date() + let logs = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0), + ActivityLog(id: "2", time: startDate.addingTimeInterval(5 * 60), step: 0, distance: 0), + ActivityLog(id: "3", time: startDate.addingTimeInterval(10 * 60), step: 0, distance: 0), + ] + dummyRepository.mockLogs = logs + let expectation = expectation(description: "All zero logs") + + // When + sut.execute() + .sink { result in + switch result { + case .success(let processedLogs): + // Then + XCTAssertEqual(processedLogs.count, 3) + // 모두 0인 경우 그대로 반환됨 + XCTAssertTrue(processedLogs.allSatisfy { $0.step == 0 && $0.distance == 0 }) + expectation.fulfill() + case .failure: + XCTFail("0 로그에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithRepositoryError_ReturnsFailure() { + // Given + dummyRepository.shouldReturnError = true + let expectation = expectation(description: "Repository error") + + // When + sut.execute() + .sink { result in + switch result { + case .success: + XCTFail("에러가 발생해야 함") + case .failure(let error): + // Then + XCTAssertNotNil(error) + expectation.fulfill() + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_AppliesCorrectionBeforeSmoothing() { + // Given + let startDate = Date() + // 첫 번째 로그와 두 번째 로그 사이에 600보 차이 (스파이크) + let logs = [ + ActivityLog(id: "1", time: startDate, step: 100, distance: 50), + ActivityLog(id: "2", time: startDate.addingTimeInterval(5 * 60), step: 700, distance: 350), + ] + dummyRepository.mockLogs = logs + let expectation = expectation(description: "Correction before smoothing") + + // When + sut.execute() + .sink { result in + switch result { + case .success(let processedLogs): + // Then + XCTAssertEqual(processedLogs.count, 2) + // 보정이 적용되어 스파이크가 완화되어야 함 + let stepDiff = processedLogs[1].step - processedLogs[0].step + XCTAssertLessThan(stepDiff, 600, "보정 후 차이가 600보다 작아야 함") + expectation.fulfill() + case .failure: + XCTFail("실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } +} diff --git a/DomainTests/UseCases/GetAverageActivityStatsUseCaseTests.swift b/DomainTests/UseCases/GetAverageActivityStatsUseCaseTests.swift new file mode 100644 index 0000000..52d1e0c --- /dev/null +++ b/DomainTests/UseCases/GetAverageActivityStatsUseCaseTests.swift @@ -0,0 +1,250 @@ +// +// GetAverageActivityStatsUseCaseTests.swift +// DomainTests +// +// Created by 김영훈 on 2025-11-28. +// + +import XCTest +import Combine +@testable import Domain +@testable import Data + +final class GetAverageActivityStatsUseCaseTests: XCTestCase { + + var sut: GetAverageActivityStatsUseCaseImpl! + var dummyRepository: DummyClimbRecordRepositoryImpl! + var cancellables: Set! + + override func setUp() { + super.setUp() + dummyRepository = DummyClimbRecordRepositoryImpl() + sut = GetAverageActivityStatsUseCaseImpl(repository: dummyRepository) + cancellables = [] + } + + override func tearDown() { + cancellables = nil + sut = nil + dummyRepository = nil + super.tearDown() + } + + // MARK: - Tests + + func test_execute_WithEmptyRecords_ReturnsEmptyStats() { + // Given + dummyRepository.mockRecords = [] + let expectation = expectation(description: "Empty records") + + // When + sut.execute() + .sink { result in + switch result { + case .success(let stats): + // Then + XCTAssertEqual(stats.averageTotalMinutes, 0) + XCTAssertEqual(stats.averageExerciseMinutes, 0) + XCTAssertEqual(stats.averageRestMinutes, 0) + XCTAssertEqual(stats.averageSpeed, 0.0) + expectation.fulfill() + case .failure: + XCTFail("빈 레코드에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithRecordsWithoutTimeLogs_ReturnsEmptyStats() { + // Given + let mountain = Mountain(id: 1, name: "한라산", address: "제주", height: 1950, isFamous: true) + let record = ClimbRecord( + id: "1", + mountain: mountain, + timeLog: [], // 시간 로그 없음 + images: [], + score: 5, + comment: "", + isBookmarked: false, + climbDate: Date() + ) + dummyRepository.mockRecords = [record] + let expectation = expectation(description: "No time logs") + + // When + sut.execute() + .sink { result in + switch result { + case .success(let stats): + // Then + XCTAssertEqual(stats.averageTotalMinutes, 0) + XCTAssertEqual(stats.averageExerciseMinutes, 0) + XCTAssertEqual(stats.averageRestMinutes, 0) + XCTAssertEqual(stats.averageSpeed, 0.0) + expectation.fulfill() + case .failure: + XCTFail("빈 시간 로그에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithSingleValidRecord_CalculatesCorrectAverages() { + // Given + let startDate = Date() + let timeLogs = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0), + ActivityLog(id: "2", time: startDate.addingTimeInterval(5 * 60), step: 150, distance: 300), // 운동 + ActivityLog(id: "3", time: startDate.addingTimeInterval(10 * 60), step: 50, distance: 100), // 휴식 + ActivityLog(id: "4", time: startDate.addingTimeInterval(15 * 60), step: 200, distance: 400), // 운동 + ] + let mountain = Mountain(id: 1, name: "한라산", address: "제주", height: 1950, isFamous: true) + let record = ClimbRecord( + id: "1", + mountain: mountain, + timeLog: timeLogs, + images: [], + score: 5, + comment: "", + isBookmarked: false, + climbDate: Date() + ) + dummyRepository.mockRecords = [record] + let expectation = expectation(description: "Single valid record") + + // When + sut.execute() + .sink { result in + switch result { + case .success(let stats): + // Then + XCTAssertEqual(stats.averageTotalMinutes, 15, "총 15분") + XCTAssertEqual(stats.averageExerciseMinutes, 10, "운동 시간 10분") + XCTAssertEqual(stats.averageRestMinutes, 5, "휴식 시간 5분") + + let expectedSpeed = 800.0 / 15.0 // 총 거리 / 총 시간 + XCTAssertEqual(stats.averageSpeed, expectedSpeed, accuracy: 0.01) + expectation.fulfill() + case .failure: + XCTFail("유효한 레코드에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithMultipleValidRecords_CalculatesCorrectAverages() { + // Given + let startDate = Date() + + // 첫 번째 레코드: 20분 등산 (운동 15분, 휴식 5분, 거리 1000m) + let timeLogs1 = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0), + ActivityLog(id: "2", time: startDate.addingTimeInterval(5 * 60), step: 150, distance: 300), + ActivityLog(id: "3", time: startDate.addingTimeInterval(10 * 60), step: 50, distance: 100), + ActivityLog(id: "4", time: startDate.addingTimeInterval(15 * 60), step: 200, distance: 400), + ActivityLog(id: "5", time: startDate.addingTimeInterval(20 * 60), step: 150, distance: 200), + ] + + // 두 번째 레코드: 10분 등산 (운동 5분, 휴식 5분, 거리 400m) + let timeLogs2 = [ + ActivityLog(id: "6", time: startDate, step: 0, distance: 0), + ActivityLog(id: "7", time: startDate.addingTimeInterval(5 * 60), step: 150, distance: 300), + ActivityLog(id: "8", time: startDate.addingTimeInterval(10 * 60), step: 50, distance: 100), + ] + + let mountain = Mountain(id: 1, name: "한라산", address: "제주", height: 1950, isFamous: true) + let record1 = ClimbRecord(id: "1", mountain: mountain, timeLog: timeLogs1, images: [], score: 5, comment: "", isBookmarked: false, climbDate: Date()) + let record2 = ClimbRecord(id: "2", mountain: mountain, timeLog: timeLogs2, images: [], score: 4, comment: "", isBookmarked: false, climbDate: Date()) + + dummyRepository.mockRecords = [record1, record2] + let expectation = expectation(description: "Multiple valid records") + + // When + sut.execute() + .sink { result in + switch result { + case .success(let stats): + // Then + // 평균 총 시간: (20 + 10) / 2 = 15분 + XCTAssertEqual(stats.averageTotalMinutes, 15) + + // 평균 운동 시간: (15 + 5) / 2 = 10분 + XCTAssertEqual(stats.averageExerciseMinutes, 10) + + // 평균 휴식 시간: (5 + 5) / 2 = 5분 + XCTAssertEqual(stats.averageRestMinutes, 5) + + // 평균 속도: 1400 / 30 ≈ 46.67 m/m + let expectedSpeed = (1000.0 + 400.0) / (20.0 + 10.0) + XCTAssertEqual(stats.averageSpeed, expectedSpeed, accuracy: 0.01) + expectation.fulfill() + case .failure: + XCTFail("유효한 레코드에서 실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithMixedRecords_FiltersOutEmptyTimeLogs() { + // Given + let startDate = Date() + let timeLogs = [ + ActivityLog(id: "1", time: startDate, step: 0, distance: 0), + ActivityLog(id: "2", time: startDate.addingTimeInterval(10 * 60), step: 200, distance: 500), + ] + + let mountain = Mountain(id: 1, name: "한라산", address: "제주", height: 1950, isFamous: true) + let recordWithLogs = ClimbRecord(id: "1", mountain: mountain, timeLog: timeLogs, images: [], score: 5, comment: "", isBookmarked: false, climbDate: Date()) + let recordWithoutLogs = ClimbRecord(id: "2", mountain: mountain, timeLog: [], images: [], score: 4, comment: "", isBookmarked: false, climbDate: Date()) + + dummyRepository.mockRecords = [recordWithLogs, recordWithoutLogs] + let expectation = expectation(description: "Mixed records") + + // When + sut.execute() + .sink { result in + switch result { + case .success(let stats): + // Then + // 빈 로그를 가진 레코드는 제외되고, 유효한 레코드 1개만 계산됨 + XCTAssertEqual(stats.averageTotalMinutes, 10) + expectation.fulfill() + case .failure: + XCTFail("실패하면 안 됨") + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } + + func test_execute_WithRepositoryError_ReturnsFailure() { + // Given + dummyRepository.shouldReturnError = true + let expectation = expectation(description: "Repository error") + + // When + sut.execute() + .sink { result in + switch result { + case .success: + XCTFail("에러가 발생해야 함") + case .failure(let error): + // Then + XCTAssertNotNil(error) + expectation.fulfill() + } + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + } +} diff --git a/DomainTests/UseCases/GetRecordStatsUseCaseTests.swift b/DomainTests/UseCases/GetRecordStatsUseCaseTests.swift new file mode 100644 index 0000000..30e3c3e --- /dev/null +++ b/DomainTests/UseCases/GetRecordStatsUseCaseTests.swift @@ -0,0 +1,150 @@ +// +// GetRecordStatsUseCaseTests.swift +// DomainTests +// +// Created by 김영훈 on 2025-11-28. +// + +import XCTest +@testable import Domain + +final class GetRecordStatsUseCaseTests: XCTestCase { + + var sut: GetRecordStatsUseCaseImpl! + + override func setUp() { + super.setUp() + sut = GetRecordStatsUseCaseImpl() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + // MARK: - Tests + + func test_execute_WithEmptyRecords_ReturnsZeroStats() { + // Given + let records: [ClimbRecord] = [] + + // When + let result = sut.execute(records: records) + + // Then + XCTAssertEqual(result.mountainCount, 0) + XCTAssertEqual(result.climbCount, 0) + XCTAssertEqual(result.totalHeight, 0) + } + + func test_execute_WithSingleRecord_ReturnsCorrectStats() { + // Given + let mountain = Mountain(id: 1, name: "한라산", address: "제주", height: 1950, isFamous: true) + let record = ClimbRecord( + id: "1", + mountain: mountain, + timeLog: [], + images: [], + score: 5, + comment: "", + isBookmarked: false, + climbDate: Date() + ) + + // When + let result = sut.execute(records: [record]) + + // Then + XCTAssertEqual(result.mountainCount, 1) + XCTAssertEqual(result.climbCount, 1) + XCTAssertEqual(result.totalHeight, 1950) + } + + func test_execute_WithMultipleRecordsSameMountain_ReturnsCorrectMountainCount() { + // Given + let mountain = Mountain(id: 1, name: "한라산", address: "제주", height: 1950, isFamous: true) + let record1 = ClimbRecord( + id: "1", + mountain: mountain, + timeLog: [], + images: [], + score: 5, + comment: "", + isBookmarked: false, + climbDate: Date() + ) + let record2 = ClimbRecord( + id: "2", + mountain: mountain, + timeLog: [], + images: [], + score: 4, + comment: "", + isBookmarked: false, + climbDate: Date() + ) + + // When + let result = sut.execute(records: [record1, record2]) + + // Then + XCTAssertEqual(result.mountainCount, 1, "같은 산을 여러 번 등산해도 고유 산 개수는 1개여야 함") + XCTAssertEqual(result.climbCount, 2, "등산 횟수는 2회여야 함") + XCTAssertEqual(result.totalHeight, 3900, "총 높이는 1950 * 2 = 3900") + } + + func test_execute_WithMultipleRecordsDifferentMountains_ReturnsCorrectStats() { + // Given + let mountain1 = Mountain(id: 1, name: "한라산", address: "제주", height: 1950, isFamous: true) + let mountain2 = Mountain(id: 2, name: "설악산", address: "강원", height: 1708, isFamous: true) + let mountain3 = Mountain(id: 3, name: "북한산", address: "서울", height: 836, isFamous: true) + + let record1 = ClimbRecord(id: "1", mountain: mountain1, timeLog: [], images: [], score: 5, comment: "", isBookmarked: false, climbDate: Date()) + let record2 = ClimbRecord(id: "2", mountain: mountain2, timeLog: [], images: [], score: 4, comment: "", isBookmarked: false, climbDate: Date()) + let record3 = ClimbRecord(id: "3", mountain: mountain3, timeLog: [], images: [], score: 3, comment: "", isBookmarked: false, climbDate: Date()) + + // When + let result = sut.execute(records: [record1, record2, record3]) + + // Then + XCTAssertEqual(result.mountainCount, 3) + XCTAssertEqual(result.climbCount, 3) + XCTAssertEqual(result.totalHeight, 4494, "총 높이는 1950 + 1708 + 836 = 4494") + } + + func test_execute_WithMountainWithoutHeight_IgnoresNilHeight() { + // Given + let mountain1 = Mountain(id: 1, name: "한라산", address: "제주", height: 1950, isFamous: true) + let mountain2 = Mountain(id: 2, name: "무명산", address: "어딘가", height: nil, isFamous: false) + + let record1 = ClimbRecord(id: "1", mountain: mountain1, timeLog: [], images: [], score: 5, comment: "", isBookmarked: false, climbDate: Date()) + let record2 = ClimbRecord(id: "2", mountain: mountain2, timeLog: [], images: [], score: 4, comment: "", isBookmarked: false, climbDate: Date()) + + // When + let result = sut.execute(records: [record1, record2]) + + // Then + XCTAssertEqual(result.mountainCount, 2) + XCTAssertEqual(result.climbCount, 2) + XCTAssertEqual(result.totalHeight, 1950, "nil 높이는 무시되고 1950만 계산됨") + } + + func test_execute_WithMixedRecords_ReturnsCorrectStats() { + // Given + let mountain1 = Mountain(id: 1, name: "한라산", address: "제주", height: 1950, isFamous: true) + let mountain2 = Mountain(id: 2, name: "설악산", address: "강원", height: 1708, isFamous: true) + + // 한라산 2회, 설악산 1회 + let record1 = ClimbRecord(id: "1", mountain: mountain1, timeLog: [], images: [], score: 5, comment: "", isBookmarked: false, climbDate: Date()) + let record2 = ClimbRecord(id: "2", mountain: mountain2, timeLog: [], images: [], score: 4, comment: "", isBookmarked: false, climbDate: Date()) + let record3 = ClimbRecord(id: "3", mountain: mountain1, timeLog: [], images: [], score: 5, comment: "", isBookmarked: false, climbDate: Date()) + + // When + let result = sut.execute(records: [record1, record2, record3]) + + // Then + XCTAssertEqual(result.mountainCount, 2, "2개의 고유한 산") + XCTAssertEqual(result.climbCount, 3, "총 3회 등산") + XCTAssertEqual(result.totalHeight, 5608, "1950 * 2 + 1708 = 5608") + } +} diff --git a/Project.swift b/Project.swift index 2100c7e..6ed717a 100644 --- a/Project.swift +++ b/Project.swift @@ -109,6 +109,23 @@ let project = Project( ] ) ), + + .target(name: "DomainTests", + destinations: [.iPhone], + product: .unitTests, + bundleId: "com.kyh.DomainTests", + deploymentTargets: .iOS(iOSVersion), + sources: ["DomainTests/**"], + dependencies: [ + .target(name: "Domain"), + .target(name: "Data") + ], + settings: .settings( + base: [ + "DEVELOPMENT_TEAM": .string(teamID) + ] + ) + ), .target(name: "Data", destinations: [.iPhone], From 049cc8b5cf3acdcd8493a2317915aaf3ede0c001 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 01:16:30 +0900 Subject: [PATCH 02/23] FetchWeeklyForecase UseCase Test --- .../Dummy/DummyClimbRecordRepositoryImpl.swift | 7 ++++--- .../Repositories/Dummy/DummyForecastRepositoryImpl.swift | 5 +++-- .../Dummy/DummyTrackActivityRepositoryImpl.swift | 5 +++-- DomainTests/UseCases/FetchWeeklyForecastUseCaseTests.swift | 1 + DomainTests/UseCases/GetActivityLogsUseCaseTests.swift | 1 + .../UseCases/GetAverageActivityStatsUseCaseTests.swift | 1 + 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Data/Sources/Repositories/Dummy/DummyClimbRecordRepositoryImpl.swift b/Data/Sources/Repositories/Dummy/DummyClimbRecordRepositoryImpl.swift index 73641d5..9b91029 100644 --- a/Data/Sources/Repositories/Dummy/DummyClimbRecordRepositoryImpl.swift +++ b/Data/Sources/Repositories/Dummy/DummyClimbRecordRepositoryImpl.swift @@ -19,17 +19,18 @@ public final class DummyClimbRecordRepositoryImpl: ClimbRecordRepository { // Test properties var mockRecords: [ClimbRecord] = [] var shouldReturnError = false + var useMockData = false init() {} - + public func fetch(keyword: String, isOnlyBookmarked: Bool) -> AnyPublisher, Never> { if shouldReturnError { return Just(.failure(NSError(domain: "Test", code: -1, userInfo: nil))) .eraseToAnyPublisher() } - // Use mockRecords if available, otherwise use dummy data - var records = mockRecords.isEmpty ? dummyClimbRecords : mockRecords + // Use mockRecords if useMockData flag is set, otherwise use dummy data + var records = useMockData ? mockRecords : dummyClimbRecords if !keyword.isEmpty { let ids = Mountain.dummy.filter { $0.name.contains(keyword)}.map { $0.id } diff --git a/Data/Sources/Repositories/Dummy/DummyForecastRepositoryImpl.swift b/Data/Sources/Repositories/Dummy/DummyForecastRepositoryImpl.swift index 328363a..ff505cb 100644 --- a/Data/Sources/Repositories/Dummy/DummyForecastRepositoryImpl.swift +++ b/Data/Sources/Repositories/Dummy/DummyForecastRepositoryImpl.swift @@ -14,6 +14,7 @@ public final class DummyForecastRepositoryImpl: ForecastRepository { // Test properties var mockForecastItems: [ForecastItem] = [] var shouldReturnError = false + var useMockData = false var lastNx: Int? var lastNy: Int? @@ -28,8 +29,8 @@ public final class DummyForecastRepositoryImpl: ForecastRepository { .eraseToAnyPublisher() } - // Use mockForecastItems if available, otherwise generate random data - if !mockForecastItems.isEmpty { + // Use mockForecastItems if useMockData flag is set + if useMockData { return Just(.success(mockForecastItems)) .eraseToAnyPublisher() } diff --git a/Data/Sources/Repositories/Dummy/DummyTrackActivityRepositoryImpl.swift b/Data/Sources/Repositories/Dummy/DummyTrackActivityRepositoryImpl.swift index 9aacb09..93331c9 100644 --- a/Data/Sources/Repositories/Dummy/DummyTrackActivityRepositoryImpl.swift +++ b/Data/Sources/Repositories/Dummy/DummyTrackActivityRepositoryImpl.swift @@ -21,6 +21,7 @@ public final class DummyTrackActivityRepositoryImpl: TrackActivityRepository { // Test properties var mockLogs: [ActivityLog] = [] var shouldReturnError = false + var useMockData = false init() {} @@ -47,8 +48,8 @@ public final class DummyTrackActivityRepositoryImpl: TrackActivityRepository { .eraseToAnyPublisher() } - // Use mockLogs if available, otherwise generate random data - if !mockLogs.isEmpty { + // Use mockLogs if useMockData flag is set + if useMockData { return Just(.success(mockLogs)) .eraseToAnyPublisher() } diff --git a/DomainTests/UseCases/FetchWeeklyForecastUseCaseTests.swift b/DomainTests/UseCases/FetchWeeklyForecastUseCaseTests.swift index 68597ad..1364e92 100644 --- a/DomainTests/UseCases/FetchWeeklyForecastUseCaseTests.swift +++ b/DomainTests/UseCases/FetchWeeklyForecastUseCaseTests.swift @@ -19,6 +19,7 @@ final class FetchWeeklyForecastUseCaseTests: XCTestCase { override func setUp() { super.setUp() dummyRepository = DummyForecastRepositoryImpl() + dummyRepository.useMockData = true sut = FetchWeeklyForecastUseCaseImpl(repository: dummyRepository) cancellables = [] } diff --git a/DomainTests/UseCases/GetActivityLogsUseCaseTests.swift b/DomainTests/UseCases/GetActivityLogsUseCaseTests.swift index 03ac8c2..1f1c9f0 100644 --- a/DomainTests/UseCases/GetActivityLogsUseCaseTests.swift +++ b/DomainTests/UseCases/GetActivityLogsUseCaseTests.swift @@ -19,6 +19,7 @@ final class GetActivityLogsUseCaseTests: XCTestCase { override func setUp() { super.setUp() dummyRepository = DummyTrackActivityRepositoryImpl() + dummyRepository.useMockData = true sut = GetActivityLogsUseCaseImpl(repository: dummyRepository) cancellables = [] } diff --git a/DomainTests/UseCases/GetAverageActivityStatsUseCaseTests.swift b/DomainTests/UseCases/GetAverageActivityStatsUseCaseTests.swift index 52d1e0c..2a1d195 100644 --- a/DomainTests/UseCases/GetAverageActivityStatsUseCaseTests.swift +++ b/DomainTests/UseCases/GetAverageActivityStatsUseCaseTests.swift @@ -19,6 +19,7 @@ final class GetAverageActivityStatsUseCaseTests: XCTestCase { override func setUp() { super.setUp() dummyRepository = DummyClimbRecordRepositoryImpl() + dummyRepository.useMockData = true sut = GetAverageActivityStatsUseCaseImpl(repository: dummyRepository) cancellables = [] } From d569ca9418d4e7aca60975f0b011a92c2bf4e9e2 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 02:02:50 +0900 Subject: [PATCH 03/23] =?UTF-8?q?usecase=20test,=20fastlane,=20github=20ac?= =?UTF-8?q?tions=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 87 +++++++++++++++ .github/workflows/deploy.yml | 86 +++++++++++++++ .gitignore | 12 +++ .ruby-version | 1 + CI_CD_SETUP.md | 202 +++++++++++++++++++++++++++++++++++ Gemfile | 6 ++ fastlane/Appfile | 9 ++ fastlane/Fastfile | 70 ++++++++++++ 8 files changed, 473 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 .ruby-version create mode 100644 CI_CD_SETUP.md create mode 100644 Gemfile create mode 100644 fastlane/Appfile create mode 100644 fastlane/Fastfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4969b49 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,87 @@ +name: CI + +on: + pull_request: + branches: [ main, develop ] + push: + branches: [ main, develop ] + +jobs: + test: + name: Run Tests + runs-on: macos-14 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.4' + + - name: Install Tuist + run: | + curl -Ls https://install.tuist.io | bash + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Cache Tuist + uses: actions/cache@v4 + with: + path: | + ~/.tuist/Cache + Tuist/.build + key: ${{ runner.os }}-tuist-${{ hashFiles('**/Project.swift') }} + restore-keys: | + ${{ runner.os }}-tuist- + + - name: Run Tests + run: bundle exec fastlane test + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: fastlane/test_output + + build: + name: Build Verification + runs-on: macos-14 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.4' + + - name: Install Tuist + run: | + curl -Ls https://install.tuist.io | bash + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Cache Tuist + uses: actions/cache@v4 + with: + path: | + ~/.tuist/Cache + Tuist/.build + key: ${{ runner.os }}-tuist-${{ hashFiles('**/Project.swift') }} + restore-keys: | + ${{ runner.os }}-tuist- + + - name: Build App + run: bundle exec fastlane build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..4a8397b --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,86 @@ +name: Deploy to TestFlight + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + deploy: + name: Deploy to TestFlight + runs-on: macos-14 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.4' + + - name: Install Tuist + run: | + curl -Ls https://install.tuist.io | bash + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Cache Tuist + uses: actions/cache@v4 + with: + path: | + ~/.tuist/Cache + Tuist/.build + key: ${{ runner.os }}-tuist-${{ hashFiles('**/Project.swift') }} + restore-keys: | + ${{ runner.os }}-tuist- + + - name: Setup App Store Connect API Key + env: + APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY }} + run: | + mkdir -p ~/.appstoreconnect/private_keys + echo "$APP_STORE_CONNECT_API_KEY_CONTENT" | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8 + + - name: Import Certificates + env: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + # Create keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security set-keychain-settings -t 3600 -u build.keychain + + # Import certificate + echo "$BUILD_CERTIFICATE_BASE64" | base64 --decode > certificate.p12 + security import certificate.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain + rm certificate.p12 + + - name: Import Provisioning Profile + env: + PROVISIONING_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }} + run: | + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles + echo "$PROVISIONING_PROFILE_BASE64" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision + + - name: Deploy to TestFlight + env: + FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} + APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} + run: bundle exec fastlane beta + + - name: Clean up keychain + if: always() + run: | + security delete-keychain build.keychain || true diff --git a/.gitignore b/.gitignore index 2ff805b..a41f34e 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,15 @@ Data/Resources/ ### Firebase ### GoogleService-Info.plist + +### Fastlane ### +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/README.md + +### Ruby ### +vendor/bundle +.bundle +Gemfile.lock diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..be94e6f --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.2.2 diff --git a/CI_CD_SETUP.md b/CI_CD_SETUP.md new file mode 100644 index 0000000..24e9449 --- /dev/null +++ b/CI_CD_SETUP.md @@ -0,0 +1,202 @@ +# CI/CD 설정 가이드 + +Oreum 프로젝트의 CI/CD 파이프라인은 fastlane과 GitHub Actions를 사용합니다. + +## 📋 목차 +- [구조](#구조) +- [로컬 설정](#로컬-설정) +- [GitHub Secrets 설정](#github-secrets-설정) +- [워크플로우](#워크플로우) +- [Fastlane 레인](#fastlane-레인) + +## 🏗 구조 + +### CI 워크플로우 (`.github/workflows/ci.yml`) +- **트리거**: PR 생성/업데이트, main/develop 브랜치에 push +- **작업**: + - 유닛 테스트 실행 + - 빌드 검증 + +### Deploy 워크플로우 (`.github/workflows/deploy.yml`) +- **트리거**: main 브랜치에 push, 수동 실행 +- **작업**: + - 빌드 번호 자동 증가 + - TestFlight에 자동 배포 + +## 🛠 로컬 설정 + +### 1. Ruby 및 Bundler 설치 +```bash +# Homebrew로 Ruby 설치 (선택사항) +brew install ruby + +# Bundler 설치 +gem install bundler +``` + +### 2. 의존성 설치 +```bash +# 프로젝트 루트에서 +bundle install +``` + +### 3. Fastlane 레인 실행 + +#### 테스트 실행 +```bash +bundle exec fastlane test +``` + +#### 빌드 검증 +```bash +bundle exec fastlane build +``` + +#### TestFlight 배포 (로컬) +```bash +bundle exec fastlane beta +``` + +## 🔐 GitHub Secrets 설정 + +TestFlight 자동 배포를 위해 다음 Secrets를 GitHub 저장소에 추가해야 합니다: + +### 필수 Secrets + +#### 1. App Store Connect API Key + +**APP_STORE_CONNECT_API_KEY_ID** +- App Store Connect API Key ID +- 형식: `ABCD1234EF` + +**APP_STORE_CONNECT_API_ISSUER_ID** +- App Store Connect Issuer ID +- 형식: `12345678-1234-1234-1234-123456789012` + +**APP_STORE_CONNECT_API_KEY** +- App Store Connect API Key 파일 내용 (base64 인코딩) +- 생성 방법: + ```bash + cat AuthKey_ABCD1234EF.p8 | base64 + ``` + +##### API Key 생성 방법: +1. [App Store Connect](https://appstoreconnect.apple.com) → Users and Access → Keys +2. "Generate API Key" 클릭 +3. Key Name 입력, Access는 "Admin" 선택 +4. .p8 파일 다운로드 및 Key ID, Issuer ID 저장 + +#### 2. 인증서 및 프로비저닝 프로파일 + +**BUILD_CERTIFICATE_BASE64** +- Distribution 인증서 (.p12 파일, base64 인코딩) +- 생성 방법: + ```bash + # Keychain에서 인증서 내보내기 (파일 이름: certificate.p12) + cat certificate.p12 | base64 + ``` + +**P12_PASSWORD** +- .p12 파일 생성 시 입력한 비밀번호 + +**PROVISIONING_PROFILE_BASE64** +- App Store 프로비저닝 프로파일 (base64 인코딩) +- 생성 방법: + ```bash + cat YourProfile.mobileprovision | base64 + ``` + +**KEYCHAIN_PASSWORD** +- CI에서 사용할 임시 키체인 비밀번호 (임의의 문자열) +- 예: `temp_keychain_password_123` + +**FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD** +- Apple ID 2단계 인증용 앱 전용 암호 +- 생성: [appleid.apple.com](https://appleid.apple.com) → Security → App-Specific Passwords + +### Secrets 추가 방법 + +1. GitHub 저장소 → Settings → Secrets and variables → Actions +2. "New repository secret" 클릭 +3. 각 Secret의 Name과 Value 입력 + +## 🔄 워크플로우 + +### CI 워크플로우 (자동) + +**트리거 조건:** +- Pull Request 생성/업데이트 (target: main, develop) +- main 또는 develop 브랜치에 push + +**실행 내용:** +1. 테스트 실행 +2. 빌드 검증 +3. 테스트 결과 아티팩트 업로드 + +### Deploy 워크플로우 (자동/수동) + +**트리거 조건:** +- main 브랜치에 push (자동) +- Actions 탭에서 수동 실행 (workflow_dispatch) + +**실행 내용:** +1. Tuist로 프로젝트 생성 +2. 빌드 번호 자동 증가 +3. 앱 빌드 +4. TestFlight 업로드 +5. 버전 변경사항 커밋 + +## 📱 Fastlane 레인 + +### `test` +- Tuist로 프로젝트 생성 +- 유닛 테스트 실행 +- 코드 커버리지 측정 + +### `build` +- Tuist로 프로젝트 생성 +- 앱 빌드 (코드 서명 없이) + +### `beta` +- Tuist로 프로젝트 생성 +- 빌드 번호 자동 증가 +- 앱 빌드 및 서명 +- TestFlight 업로드 +- 버전 변경사항 커밋 + +## 🚨 주의사항 + +1. **Apple ID**: Fastfile의 `apple_id` 값을 실제 Apple ID로 변경하세요 +2. **Team ID**: Project.swift의 `teamID`가 올바른지 확인하세요 +3. **Bundle ID**: `com.kyh.Oreum`이 맞는지 확인하세요 +4. **인증서**: Distribution 인증서와 App Store 프로비저닝 프로파일이 필요합니다 +5. **Match 사용**: 팀에서 인증서를 공유하려면 `match` 사용을 권장합니다 + +## 🔧 트러블슈팅 + +### 테스트 실패 +```bash +# 로컬에서 테스트 실행하여 확인 +bundle exec fastlane test +``` + +### 빌드 실패 +```bash +# Tuist 캐시 삭제 +tuist clean + +# 프로젝트 재생성 +tuist generate +``` + +### 코드 서명 오류 +- Xcode에서 Signing & Capabilities 탭 확인 +- 인증서와 프로비저닝 프로파일이 유효한지 확인 +- GitHub Secrets가 올바르게 설정되었는지 확인 + +## 📚 참고 자료 + +- [Fastlane 문서](https://docs.fastlane.tools/) +- [GitHub Actions 문서](https://docs.github.com/en/actions) +- [Tuist 문서](https://docs.tuist.io/) +- [App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi) diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..3a5f03e --- /dev/null +++ b/Gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +ruby ">= 3.0.0" + +gem "fastlane" +gem "cocoapods", "~> 1.15" diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..fc161df --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,9 @@ +# Appfile for Oreum + +app_identifier("com.kyh.Oreum") +apple_id("kmyghn@gmail.com") + +team_id("4QUWH828P3") + +# For more information about the Appfile, see: +# https://docs.fastlane.tools/advanced/#appfile diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..eb14ac9 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,70 @@ +# Fastfile for Oreum + +default_platform(:ios) + +platform :ios do + desc "Run tests" + lane :test do + # Tuist generate to create Xcode project + sh("cd .. && tuist generate") + + # Run tests + run_tests( + workspace: "Oreum.xcworkspace", + scheme: "Domain", # Use Domain scheme which includes DomainTests + devices: ["iPhone 15 Pro"], + clean: true, + code_coverage: true + ) + end + + desc "Build the app" + lane :build do + # Tuist generate to create Xcode project + sh("cd .. && tuist generate") + + # Build the app + build_app( + workspace: "Oreum.xcworkspace", + scheme: "Oreum", + clean: true, + skip_archive: true, + skip_codesigning: true + ) + end + + desc "Deploy to TestFlight" + lane :beta do + # Tuist generate to create Xcode project + sh("cd .. && tuist generate") + + # Increment build number + increment_build_number( + xcodeproj: "Oreum.xcodeproj" + ) + + # Match for code signing (or use manual signing) + # match( + # type: "appstore", + # readonly: true + # ) + + # Build and upload to TestFlight + build_app( + workspace: "Oreum.xcworkspace", + scheme: "Oreum", + export_method: "app-store" + ) + + upload_to_testflight( + skip_waiting_for_build_processing: true, + apple_id: "6737080550" # Replace with your app's Apple ID + ) + + # Commit version bump + commit_version_bump( + message: "Version Bump by fastlane", + xcodeproj: "Oreum.xcodeproj" + ) + end +end From 04abd3379e564936f9941257906d3f5b4edbe6ea Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 02:11:18 +0900 Subject: [PATCH 04/23] =?UTF-8?q?=EB=B2=84=EC=A0=84=20=EC=88=98=EB=8F=99?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC,=20=EC=8B=9C=EB=AE=AC=20=EA=B8=B0?= =?UTF-8?q?=EA=B8=B0=2017=ED=94=84=EB=A1=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CI_CD_SETUP.md | 61 +++++++++++++++++++++++++++++++++-------------- fastlane/Fastfile | 13 +--------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/CI_CD_SETUP.md b/CI_CD_SETUP.md index 24e9449..fb35c12 100644 --- a/CI_CD_SETUP.md +++ b/CI_CD_SETUP.md @@ -20,18 +20,28 @@ Oreum 프로젝트의 CI/CD 파이프라인은 fastlane과 GitHub Actions를 사 ### Deploy 워크플로우 (`.github/workflows/deploy.yml`) - **트리거**: main 브랜치에 push, 수동 실행 - **작업**: - - 빌드 번호 자동 증가 - TestFlight에 자동 배포 ## 🛠 로컬 설정 -### 1. Ruby 및 Bundler 설치 +### 1. Ruby 설정 (rbenv 사용) ```bash -# Homebrew로 Ruby 설치 (선택사항) -brew install ruby +# rbenv 설치 +brew install rbenv ruby-build -# Bundler 설치 -gem install bundler +# rbenv 초기화 (zshrc에 추가) +echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc +source ~/.zshrc + +# Ruby 3.2.2 설치 +rbenv install 3.2.2 + +# 프로젝트 디렉토리에서 Ruby 버전 설정 +cd /path/to/Oreum +rbenv local 3.2.2 + +# 버전 확인 +ruby -v # ruby 3.2.2 출력 확인 ``` ### 2. 의존성 설치 @@ -141,39 +151,54 @@ TestFlight 자동 배포를 위해 다음 Secrets를 GitHub 저장소에 추가 **실행 내용:** 1. Tuist로 프로젝트 생성 -2. 빌드 번호 자동 증가 -3. 앱 빌드 -4. TestFlight 업로드 -5. 버전 변경사항 커밋 +2. 앱 빌드 +3. TestFlight 업로드 ## 📱 Fastlane 레인 ### `test` - Tuist로 프로젝트 생성 -- 유닛 테스트 실행 +- 유닛 테스트 실행 (Domain 스킴 사용) - 코드 커버리지 측정 +> **참고**: DomainTests 타겟의 테스트를 실행하기 위해 Domain 스킴을 사용합니다. + ### `build` - Tuist로 프로젝트 생성 - 앱 빌드 (코드 서명 없이) ### `beta` - Tuist로 프로젝트 생성 -- 빌드 번호 자동 증가 - 앱 빌드 및 서명 - TestFlight 업로드 -- 버전 변경사항 커밋 + +> **참고**: 빌드 번호는 `Project.swift` 파일에서 수동으로 관리합니다. +> 배포 전에 `let buildNumber = "X"` 값을 직접 변경하세요. ## 🚨 주의사항 -1. **Apple ID**: Fastfile의 `apple_id` 값을 실제 Apple ID로 변경하세요 -2. **Team ID**: Project.swift의 `teamID`가 올바른지 확인하세요 -3. **Bundle ID**: `com.kyh.Oreum`이 맞는지 확인하세요 -4. **인증서**: Distribution 인증서와 App Store 프로비저닝 프로파일이 필요합니다 -5. **Match 사용**: 팀에서 인증서를 공유하려면 `match` 사용을 권장합니다 +1. **Tuist 프로젝트**: 이 프로젝트는 Tuist로 관리되므로 `.xcodeproj`와 `.xcworkspace` 파일은 `.gitignore`에 포함됩니다 +2. **빌드 번호**: `Project.swift` 파일에서 수동으로 관리합니다. TestFlight 배포 전에 빌드 번호를 증가시키세요 +3. **Apple ID**: `fastlane/Appfile`의 `apple_id` 값이 올바른지 확인하세요 +4. **Team ID**: `Project.swift`의 `teamID`가 올바른지 확인하세요 +5. **Bundle ID**: `com.kyh.Oreum`이 맞는지 확인하세요 +6. **인증서**: Distribution 인증서와 App Store 프로비저닝 프로파일이 필요합니다 +7. **Match 사용**: 팀에서 인증서를 공유하려면 `match` 사용을 권장합니다 ## 🔧 트러블슈팅 +### Ruby 버전 오류 +```bash +# Ruby 버전 확인 +ruby -v + +# 시스템 Ruby를 사용 중이면 rbenv 다시 초기화 +eval "$(rbenv init - zsh)" +ruby -v # 3.2.2 확인 + +# 또는 새 터미널 창 열기 +``` + ### 테스트 실패 ```bash # 로컬에서 테스트 실행하여 확인 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index eb14ac9..d6d4af3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -12,7 +12,7 @@ platform :ios do run_tests( workspace: "Oreum.xcworkspace", scheme: "Domain", # Use Domain scheme which includes DomainTests - devices: ["iPhone 15 Pro"], + devices: ["iPhone 17 Pro"], clean: true, code_coverage: true ) @@ -38,11 +38,6 @@ platform :ios do # Tuist generate to create Xcode project sh("cd .. && tuist generate") - # Increment build number - increment_build_number( - xcodeproj: "Oreum.xcodeproj" - ) - # Match for code signing (or use manual signing) # match( # type: "appstore", @@ -60,11 +55,5 @@ platform :ios do skip_waiting_for_build_processing: true, apple_id: "6737080550" # Replace with your app's Apple ID ) - - # Commit version bump - commit_version_bump( - message: "Version Bump by fastlane", - xcodeproj: "Oreum.xcodeproj" - ) end end From 68d7382138ddf7ba0c7937652a3ecc4da71f7b64 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 23:13:06 +0900 Subject: [PATCH 05/23] =?UTF-8?q?=EC=8B=9C=ED=81=AC=EB=A6=BF=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 16 ++++++ .github/workflows/deploy.yml | 8 +++ CI_CD_SETUP.md | 37 ++++++++++++- SECRET_FILES_SETUP.md | 100 +++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 SECRET_FILES_SETUP.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4969b49..d8d4455 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,14 @@ jobs: restore-keys: | ${{ runner.os }}-tuist- + - name: Create APIInfos.swift + run: | + echo "${{ secrets.API_INFOS_SWIFT }}" | base64 --decode > Data/Sources/Network/Secrets/APIInfos.swift + + - name: Create GoogleService-Info.plist + run: | + echo "${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}" | base64 --decode > Oreum/Resources/GoogleService-Info.plist + - name: Run Tests run: bundle exec fastlane test @@ -83,5 +91,13 @@ jobs: restore-keys: | ${{ runner.os }}-tuist- + - name: Create APIInfos.swift + run: | + echo "${{ secrets.API_INFOS_SWIFT }}" | base64 --decode > Data/Sources/Network/Secrets/APIInfos.swift + + - name: Create GoogleService-Info.plist + run: | + echo "${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}" | base64 --decode > Oreum/Resources/GoogleService-Info.plist + - name: Build App run: bundle exec fastlane build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4a8397b..7cb8055 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,6 +41,14 @@ jobs: restore-keys: | ${{ runner.os }}-tuist- + - name: Create APIInfos.swift + run: | + echo "${{ secrets.API_INFOS_SWIFT }}" | base64 --decode > Data/Sources/Network/Secrets/APIInfos.swift + + - name: Create GoogleService-Info.plist + run: | + echo "${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}" | base64 --decode > Oreum/Resources/GoogleService-Info.plist + - name: Setup App Store Connect API Key env: APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY }} diff --git a/CI_CD_SETUP.md b/CI_CD_SETUP.md index fb35c12..273b746 100644 --- a/CI_CD_SETUP.md +++ b/CI_CD_SETUP.md @@ -73,7 +73,25 @@ TestFlight 자동 배포를 위해 다음 Secrets를 GitHub 저장소에 추가 ### 필수 Secrets -#### 1. App Store Connect API Key +#### 1. 앱 필수 파일 (Secret Files) + +**API_INFOS_SWIFT** +- APIInfos.swift 파일 내용 (base64 인코딩) +- API 키들을 포함하는 Swift 파일 +- 생성 방법: + ```bash + cat Data/Sources/Network/Secrets/APIInfos.swift | base64 + ``` + +**GOOGLE_SERVICE_INFO_PLIST** +- GoogleService-Info.plist 파일 내용 (base64 인코딩) +- Firebase 설정 파일 +- 생성 방법: + ```bash + cat Oreum/Resources/GoogleService-Info.plist | base64 + ``` + +#### 2. App Store Connect API Key **APP_STORE_CONNECT_API_KEY_ID** - App Store Connect API Key ID @@ -96,7 +114,7 @@ TestFlight 자동 배포를 위해 다음 Secrets를 GitHub 저장소에 추가 3. Key Name 입력, Access는 "Admin" 선택 4. .p8 파일 다운로드 및 Key ID, Issuer ID 저장 -#### 2. 인증서 및 프로비저닝 프로파일 +#### 3. 인증서 및 프로비저닝 프로파일 **BUILD_CERTIFICATE_BASE64** - Distribution 인증서 (.p12 파일, base64 인코딩) @@ -124,6 +142,21 @@ TestFlight 자동 배포를 위해 다음 Secrets를 GitHub 저장소에 추가 - Apple ID 2단계 인증용 앱 전용 암호 - 생성: [appleid.apple.com](https://appleid.apple.com) → Security → App-Specific Passwords +### Secrets 요약 + +총 **10개**의 GitHub Secrets가 필요합니다: + +1. **API_INFOS_SWIFT** - API 키 파일 +2. **GOOGLE_SERVICE_INFO_PLIST** - Firebase 설정 파일 +3. **APP_STORE_CONNECT_API_KEY_ID** - App Store Connect API Key ID +4. **APP_STORE_CONNECT_API_ISSUER_ID** - App Store Connect Issuer ID +5. **APP_STORE_CONNECT_API_KEY** - App Store Connect API Key (base64) +6. **BUILD_CERTIFICATE_BASE64** - Distribution 인증서 (base64) +7. **P12_PASSWORD** - 인증서 비밀번호 +8. **PROVISIONING_PROFILE_BASE64** - 프로비저닝 프로파일 (base64) +9. **KEYCHAIN_PASSWORD** - CI 키체인 비밀번호 (임의 설정) +10. **FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD** - Apple 앱 전용 암호 + ### Secrets 추가 방법 1. GitHub 저장소 → Settings → Secrets and variables → Actions diff --git a/SECRET_FILES_SETUP.md b/SECRET_FILES_SETUP.md new file mode 100644 index 0000000..f8957c0 --- /dev/null +++ b/SECRET_FILES_SETUP.md @@ -0,0 +1,100 @@ +# Secret Files 설정 가이드 + +이 프로젝트는 Git에서 제외된 2개의 중요 파일이 필요합니다. CI/CD에서 이 파일들을 사용하려면 GitHub Secrets에 등록해야 합니다. + +## 📋 필요한 Secret Files + +### 1. APIInfos.swift +- **위치**: `Data/Sources/Network/Secrets/APIInfos.swift` +- **용도**: 외부 API 키 관리 +- **내용**: + - Geocoder API (vworld.kr) + - Forecast API (kma.go.kr) + - Mountain API (data.go.kr) + - MountainImage URL + +### 2. GoogleService-Info.plist +- **위치**: `Oreum/Resources/GoogleService-Info.plist` +- **용도**: Firebase 설정 +- **출처**: Firebase Console에서 다운로드 + +## 🔐 GitHub Secrets 등록 방법 + +### Step 1: APIInfos.swift 등록 + +1. 터미널에서 base64 인코딩: +```bash +cd /Users/kyh/Desktop/iOS/Oreum +cat Data/Sources/Network/Secrets/APIInfos.swift | base64 | pbcopy +``` + +2. GitHub 저장소 → Settings → Secrets and variables → Actions +3. "New repository secret" 클릭 +4. Name: `API_INFOS_SWIFT` +5. Value: 클립보드에 복사된 값 붙여넣기 +6. "Add secret" 클릭 + +### Step 2: GoogleService-Info.plist 등록 + +1. 터미널에서 base64 인코딩: +```bash +cd /Users/kyh/Desktop/iOS/Oreum +cat Oreum/Resources/GoogleService-Info.plist | base64 | pbcopy +``` + +2. GitHub 저장소 → Settings → Secrets and variables → Actions +3. "New repository secret" 클릭 +4. Name: `GOOGLE_SERVICE_INFO_PLIST` +5. Value: 클립보드에 복사된 값 붙여넣기 +6. "Add secret" 클릭 + +## ✅ 확인 방법 + +Secret이 올바르게 등록되었는지 확인: + +1. GitHub 저장소 → Settings → Secrets and variables → Actions +2. 다음 2개의 Secret이 보여야 함: + - `API_INFOS_SWIFT` + - `GOOGLE_SERVICE_INFO_PLIST` + +## 🔄 CI/CD 동작 방식 + +GitHub Actions 워크플로우가 실행될 때: + +1. **CI 워크플로우** (.github/workflows/ci.yml) + - Secret에서 base64 디코딩 + - `Data/Sources/Network/Secrets/APIInfos.swift` 생성 + - `Oreum/Resources/GoogleService-Info.plist` 생성 + - 테스트 및 빌드 실행 + +2. **Deploy 워크플로우** (.github/workflows/deploy.yml) + - Secret에서 base64 디코딩 + - `Data/Sources/Network/Secrets/APIInfos.swift` 생성 + - `Oreum/Resources/GoogleService-Info.plist` 생성 + - TestFlight 배포 + +## ⚠️ 주의사항 + +1. **로컬 파일 보호**: 이 파일들은 `.gitignore`에 포함되어 있어 Git에 커밋되지 않습니다 +2. **Secret 보안**: GitHub Secrets는 암호화되어 저장되며, 로그에 출력되지 않습니다 +3. **팀 공유**: 팀원들도 각자의 로컬 환경에 이 파일들이 필요합니다 +4. **업데이트**: API 키나 Firebase 설정이 변경되면 로컬 파일과 GitHub Secret 모두 업데이트해야 합니다 + +## 🔧 트러블슈팅 + +### 빌드 실패: "No such file or directory" +- GitHub Secrets에 `API_INFOS_SWIFT`와 `GOOGLE_SERVICE_INFO_PLIST`가 등록되어 있는지 확인 +- Secret 값이 base64로 인코딩되어 있는지 확인 + +### 런타임 에러: Firebase 초기화 실패 +- `GOOGLE_SERVICE_INFO_PLIST` Secret의 값이 올바른지 확인 +- Firebase Console에서 최신 GoogleService-Info.plist를 다시 다운로드하여 업데이트 + +### API 호출 실패 +- `API_INFOS_SWIFT` Secret의 값이 올바른지 확인 +- API 키가 만료되지 않았는지 확인 + +## 📚 관련 문서 + +- [CI/CD 설정 가이드](./CI_CD_SETUP.md) +- [GitHub Secrets 문서](https://docs.github.com/en/actions/security-guides/encrypted-secrets) From 42bb8129669b465c1e340299a0d802e036f95540 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 23:22:22 +0900 Subject: [PATCH 06/23] =?UTF-8?q?workflow=20=ED=8C=8C=EC=9D=BC=20install?= =?UTF-8?q?=20tuist=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 4 ++-- .github/workflows/deploy.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8d4455..73e1312 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: - name: Install Tuist run: | - curl -Ls https://install.tuist.io | bash + curl -Ls https://get.tuist.io | bash - name: Setup Ruby uses: ruby/setup-ruby@v1 @@ -73,7 +73,7 @@ jobs: - name: Install Tuist run: | - curl -Ls https://install.tuist.io | bash + curl -Ls https://get.tuist.io | bash - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7cb8055..ae20141 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,7 +23,7 @@ jobs: - name: Install Tuist run: | - curl -Ls https://install.tuist.io | bash + curl -Ls https://get.tuist.io | bash - name: Setup Ruby uses: ruby/setup-ruby@v1 From 15cfef087752dde34be6c881643bdd28ee5d8a9f Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 23:30:53 +0900 Subject: [PATCH 07/23] =?UTF-8?q?secret=20=ED=8C=8C=EC=9D=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=A0=84=20=ED=8F=B4=EB=8D=94=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 4 ++++ .github/workflows/deploy.yml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73e1312..3d84a53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,10 +42,12 @@ jobs: - name: Create APIInfos.swift run: | + mkdir -p Data/Sources/Network/Secrets echo "${{ secrets.API_INFOS_SWIFT }}" | base64 --decode > Data/Sources/Network/Secrets/APIInfos.swift - name: Create GoogleService-Info.plist run: | + mkdir -p Oreum/Resources echo "${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}" | base64 --decode > Oreum/Resources/GoogleService-Info.plist - name: Run Tests @@ -93,10 +95,12 @@ jobs: - name: Create APIInfos.swift run: | + mkdir -p Data/Sources/Network/Secrets echo "${{ secrets.API_INFOS_SWIFT }}" | base64 --decode > Data/Sources/Network/Secrets/APIInfos.swift - name: Create GoogleService-Info.plist run: | + mkdir -p Oreum/Resources echo "${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}" | base64 --decode > Oreum/Resources/GoogleService-Info.plist - name: Build App diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ae20141..936ce9d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -43,10 +43,12 @@ jobs: - name: Create APIInfos.swift run: | + mkdir -p Data/Sources/Network/Secrets echo "${{ secrets.API_INFOS_SWIFT }}" | base64 --decode > Data/Sources/Network/Secrets/APIInfos.swift - name: Create GoogleService-Info.plist run: | + mkdir -p Oreum/Resources echo "${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}" | base64 --decode > Oreum/Resources/GoogleService-Info.plist - name: Setup App Store Connect API Key From 93ad41eee33a1cc21307493c5aa2e6f9a308fc13 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 23:35:56 +0900 Subject: [PATCH 08/23] =?UTF-8?q?tuist=20path=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastlane/Fastfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d6d4af3..8cc7eaa 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -6,7 +6,7 @@ platform :ios do desc "Run tests" lane :test do # Tuist generate to create Xcode project - sh("cd .. && tuist generate") + sh("export PATH=\"$HOME/.tuist/bin:$PATH\" && cd .. && tuist generate") # Run tests run_tests( @@ -21,7 +21,7 @@ platform :ios do desc "Build the app" lane :build do # Tuist generate to create Xcode project - sh("cd .. && tuist generate") + sh("export PATH=\"$HOME/.tuist/bin:$PATH\" && cd .. && tuist generate") # Build the app build_app( @@ -36,7 +36,7 @@ platform :ios do desc "Deploy to TestFlight" lane :beta do # Tuist generate to create Xcode project - sh("cd .. && tuist generate") + sh("export PATH=\"$HOME/.tuist/bin:$PATH\" && cd .. && tuist generate") # Match for code signing (or use manual signing) # match( From 1026c4da959533bbf512684c9fc87ac0312897c0 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 23:38:54 +0900 Subject: [PATCH 09/23] =?UTF-8?q?workflow=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 ++ .github/workflows/deploy.yml | 1 + fastlane/Fastfile | 6 +++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d84a53..8770374 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: - name: Install Tuist run: | curl -Ls https://get.tuist.io | bash + echo "$HOME/.tuist/bin" >> $GITHUB_PATH - name: Setup Ruby uses: ruby/setup-ruby@v1 @@ -76,6 +77,7 @@ jobs: - name: Install Tuist run: | curl -Ls https://get.tuist.io | bash + echo "$HOME/.tuist/bin" >> $GITHUB_PATH - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 936ce9d..dc11074 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,6 +24,7 @@ jobs: - name: Install Tuist run: | curl -Ls https://get.tuist.io | bash + echo "$HOME/.tuist/bin" >> $GITHUB_PATH - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8cc7eaa..d6d4af3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -6,7 +6,7 @@ platform :ios do desc "Run tests" lane :test do # Tuist generate to create Xcode project - sh("export PATH=\"$HOME/.tuist/bin:$PATH\" && cd .. && tuist generate") + sh("cd .. && tuist generate") # Run tests run_tests( @@ -21,7 +21,7 @@ platform :ios do desc "Build the app" lane :build do # Tuist generate to create Xcode project - sh("export PATH=\"$HOME/.tuist/bin:$PATH\" && cd .. && tuist generate") + sh("cd .. && tuist generate") # Build the app build_app( @@ -36,7 +36,7 @@ platform :ios do desc "Deploy to TestFlight" lane :beta do # Tuist generate to create Xcode project - sh("export PATH=\"$HOME/.tuist/bin:$PATH\" && cd .. && tuist generate") + sh("cd .. && tuist generate") # Match for code signing (or use manual signing) # match( From 64c5422f9cff458df153740269878eed04547f1c Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 23:42:05 +0900 Subject: [PATCH 10/23] =?UTF-8?q?path=20=EB=AA=85=EC=8B=9C=EC=A0=81=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 8 ++++++-- .github/workflows/deploy.yml | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8770374..351c731 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,9 @@ jobs: echo "${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}" | base64 --decode > Oreum/Resources/GoogleService-Info.plist - name: Run Tests - run: bundle exec fastlane test + run: | + export PATH="$HOME/.tuist/bin:$PATH" + bundle exec fastlane test - name: Upload Test Results if: always() @@ -106,4 +108,6 @@ jobs: echo "${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}" | base64 --decode > Oreum/Resources/GoogleService-Info.plist - name: Build App - run: bundle exec fastlane build + run: | + export PATH="$HOME/.tuist/bin:$PATH" + bundle exec fastlane build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dc11074..dcc9f54 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -89,7 +89,9 @@ jobs: FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} - run: bundle exec fastlane beta + run: | + export PATH="$HOME/.tuist/bin:$PATH" + bundle exec fastlane beta - name: Clean up keychain if: always() From eef651ef2be4563213c7aa5cd75b9918f21853a3 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 23:44:05 +0900 Subject: [PATCH 11/23] =?UTF-8?q?fastlane=EC=97=90=EC=84=9C=EB=8F=84=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C=EC=A0=81=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastlane/Fastfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d6d4af3..8cc7eaa 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -6,7 +6,7 @@ platform :ios do desc "Run tests" lane :test do # Tuist generate to create Xcode project - sh("cd .. && tuist generate") + sh("export PATH=\"$HOME/.tuist/bin:$PATH\" && cd .. && tuist generate") # Run tests run_tests( @@ -21,7 +21,7 @@ platform :ios do desc "Build the app" lane :build do # Tuist generate to create Xcode project - sh("cd .. && tuist generate") + sh("export PATH=\"$HOME/.tuist/bin:$PATH\" && cd .. && tuist generate") # Build the app build_app( @@ -36,7 +36,7 @@ platform :ios do desc "Deploy to TestFlight" lane :beta do # Tuist generate to create Xcode project - sh("cd .. && tuist generate") + sh("export PATH=\"$HOME/.tuist/bin:$PATH\" && cd .. && tuist generate") # Match for code signing (or use manual signing) # match( From a9b6fae90fca07da7718164a2ff4be53f14e794d Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 23:47:52 +0900 Subject: [PATCH 12/23] =?UTF-8?q?tuist=20=EC=84=A4=EC=B9=98=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 12 ++++-------- .github/workflows/deploy.yml | 6 ++---- .tool-versions | 1 + fastlane/Fastfile | 6 +++--- 4 files changed, 10 insertions(+), 15 deletions(-) create mode 100644 .tool-versions diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 351c731..68aa14c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,8 @@ jobs: with: xcode-version: '15.4' - - name: Install Tuist - run: | - curl -Ls https://get.tuist.io | bash - echo "$HOME/.tuist/bin" >> $GITHUB_PATH + - name: Install Mise and Tuist + uses: jdx/mise-action@v2 - name: Setup Ruby uses: ruby/setup-ruby@v1 @@ -76,10 +74,8 @@ jobs: with: xcode-version: '15.4' - - name: Install Tuist - run: | - curl -Ls https://get.tuist.io | bash - echo "$HOME/.tuist/bin" >> $GITHUB_PATH + - name: Install Mise and Tuist + uses: jdx/mise-action@v2 - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dcc9f54..58d56ff 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,10 +21,8 @@ jobs: with: xcode-version: '15.4' - - name: Install Tuist - run: | - curl -Ls https://get.tuist.io | bash - echo "$HOME/.tuist/bin" >> $GITHUB_PATH + - name: Install Mise and Tuist + uses: jdx/mise-action@v2 - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..99bdd2e --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +tuist 4.36.0 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8cc7eaa..31fae9a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -6,7 +6,7 @@ platform :ios do desc "Run tests" lane :test do # Tuist generate to create Xcode project - sh("export PATH=\"$HOME/.tuist/bin:$PATH\" && cd .. && tuist generate") + sh("cd .. && mise exec -- tuist generate") # Run tests run_tests( @@ -21,7 +21,7 @@ platform :ios do desc "Build the app" lane :build do # Tuist generate to create Xcode project - sh("export PATH=\"$HOME/.tuist/bin:$PATH\" && cd .. && tuist generate") + sh("cd .. && mise exec -- tuist generate") # Build the app build_app( @@ -36,7 +36,7 @@ platform :ios do desc "Deploy to TestFlight" lane :beta do # Tuist generate to create Xcode project - sh("export PATH=\"$HOME/.tuist/bin:$PATH\" && cd .. && tuist generate") + sh("cd .. && mise exec -- tuist generate") # Match for code signing (or use manual signing) # match( From ab1c51d750ac57d7ed5a0bb961f6820892e52bb8 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 23:52:05 +0900 Subject: [PATCH 13/23] =?UTF-8?q?tuist=20=EC=84=A4=EC=B9=98=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 12 ++++++++---- .github/workflows/deploy.yml | 6 ++++-- .tool-versions | 1 - fastlane/Fastfile | 6 +++--- 4 files changed, 15 insertions(+), 10 deletions(-) delete mode 100644 .tool-versions diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68aa14c..c4ad17f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,10 @@ jobs: with: xcode-version: '15.4' - - name: Install Mise and Tuist - uses: jdx/mise-action@v2 + - name: Install Tuist + run: | + curl -Ls https://get.tuist.io | bash + echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Setup Ruby uses: ruby/setup-ruby@v1 @@ -74,8 +76,10 @@ jobs: with: xcode-version: '15.4' - - name: Install Mise and Tuist - uses: jdx/mise-action@v2 + - name: Install Tuist + run: | + curl -Ls https://get.tuist.io | bash + echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 58d56ff..c51fe2a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,8 +21,10 @@ jobs: with: xcode-version: '15.4' - - name: Install Mise and Tuist - uses: jdx/mise-action@v2 + - name: Install Tuist + run: | + curl -Ls https://get.tuist.io | bash + echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index 99bdd2e..0000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -tuist 4.36.0 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 31fae9a..d6d4af3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -6,7 +6,7 @@ platform :ios do desc "Run tests" lane :test do # Tuist generate to create Xcode project - sh("cd .. && mise exec -- tuist generate") + sh("cd .. && tuist generate") # Run tests run_tests( @@ -21,7 +21,7 @@ platform :ios do desc "Build the app" lane :build do # Tuist generate to create Xcode project - sh("cd .. && mise exec -- tuist generate") + sh("cd .. && tuist generate") # Build the app build_app( @@ -36,7 +36,7 @@ platform :ios do desc "Deploy to TestFlight" lane :beta do # Tuist generate to create Xcode project - sh("cd .. && mise exec -- tuist generate") + sh("cd .. && tuist generate") # Match for code signing (or use manual signing) # match( From d99d47c342b1e6d5f2b9bb81c6681c3c8df91172 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 23:55:14 +0900 Subject: [PATCH 14/23] =?UTF-8?q?local/bin=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 4 ++-- .github/workflows/deploy.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4ad17f..8409ec5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: - name: Run Tests run: | - export PATH="$HOME/.tuist/bin:$PATH" + export PATH="$HOME/.local/bin:$PATH" bundle exec fastlane test - name: Upload Test Results @@ -109,5 +109,5 @@ jobs: - name: Build App run: | - export PATH="$HOME/.tuist/bin:$PATH" + export PATH="$HOME/.local/bin:$PATH" bundle exec fastlane build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c51fe2a..d104ecf 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -90,7 +90,7 @@ jobs: APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} run: | - export PATH="$HOME/.tuist/bin:$PATH" + export PATH="$HOME/.local/bin:$PATH" bundle exec fastlane beta - name: Clean up keychain From 85f0c23928951fa1aaf8249a990aa69600a4797c Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Fri, 28 Nov 2025 23:57:23 +0900 Subject: [PATCH 15/23] =?UTF-8?q?fastlane=20path=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastlane/Fastfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d6d4af3..886a4ec 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -6,7 +6,7 @@ platform :ios do desc "Run tests" lane :test do # Tuist generate to create Xcode project - sh("cd .. && tuist generate") + sh("export PATH=\"$HOME/.local/bin:$PATH\" && cd .. && tuist generate") # Run tests run_tests( @@ -21,7 +21,7 @@ platform :ios do desc "Build the app" lane :build do # Tuist generate to create Xcode project - sh("cd .. && tuist generate") + sh("export PATH=\"$HOME/.local/bin:$PATH\" && cd .. && tuist generate") # Build the app build_app( @@ -36,7 +36,7 @@ platform :ios do desc "Deploy to TestFlight" lane :beta do # Tuist generate to create Xcode project - sh("cd .. && tuist generate") + sh("export PATH=\"$HOME/.local/bin:$PATH\" && cd .. && tuist generate") # Match for code signing (or use manual signing) # match( From c5ebebd0263f01cae62132c93620dceadd80aaa6 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Sat, 29 Nov 2025 00:12:17 +0900 Subject: [PATCH 16/23] =?UTF-8?q?tuist=20=EC=9C=84=EC=B9=98=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 6 ++++++ fastlane/Fastfile | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8409ec5..06bfe87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,9 @@ jobs: - name: Install Tuist run: | curl -Ls https://get.tuist.io | bash + echo "Checking tuist installation..." + ls -la $HOME/.local/bin/ || echo ".local/bin not found" + find $HOME -name "tuist" 2>/dev/null || echo "tuist binary not found" echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Setup Ruby @@ -79,6 +82,9 @@ jobs: - name: Install Tuist run: | curl -Ls https://get.tuist.io | bash + echo "Checking tuist installation..." + ls -la $HOME/.local/bin/ || echo ".local/bin not found" + find $HOME -name "tuist" 2>/dev/null || echo "tuist binary not found" echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Setup Ruby diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 886a4ec..26f007f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -6,7 +6,7 @@ platform :ios do desc "Run tests" lane :test do # Tuist generate to create Xcode project - sh("export PATH=\"$HOME/.local/bin:$PATH\" && cd .. && tuist generate") + sh("cd .. && $HOME/.local/bin/tuist generate") # Run tests run_tests( @@ -21,7 +21,7 @@ platform :ios do desc "Build the app" lane :build do # Tuist generate to create Xcode project - sh("export PATH=\"$HOME/.local/bin:$PATH\" && cd .. && tuist generate") + sh("cd .. && $HOME/.local/bin/tuist generate") # Build the app build_app( @@ -36,7 +36,7 @@ platform :ios do desc "Deploy to TestFlight" lane :beta do # Tuist generate to create Xcode project - sh("export PATH=\"$HOME/.local/bin:$PATH\" && cd .. && tuist generate") + sh("cd .. && $HOME/.local/bin/tuist generate") # Match for code signing (or use manual signing) # match( From e3686c980aed2cf8bd19099c34b09132e12a71cf Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Sat, 29 Nov 2025 00:14:16 +0900 Subject: [PATCH 17/23] =?UTF-8?q?tuist=20=EC=9C=84=EC=B9=98=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06bfe87..11ed2aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,10 +22,13 @@ jobs: - name: Install Tuist run: | + echo "Installing Tuist..." curl -Ls https://get.tuist.io | bash - echo "Checking tuist installation..." - ls -la $HOME/.local/bin/ || echo ".local/bin not found" - find $HOME -name "tuist" 2>/dev/null || echo "tuist binary not found" + echo "Installation complete. Searching for tuist..." + which tuist || echo "tuist not in PATH" + find $HOME -name "tuist" -type f 2>/dev/null | head -5 + ls -la $HOME/.local/bin/ 2>/dev/null || echo ".local/bin not found" + ls -la $HOME/.tuist/ 2>/dev/null || echo ".tuist not found" echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Setup Ruby @@ -81,10 +84,13 @@ jobs: - name: Install Tuist run: | + echo "Installing Tuist..." curl -Ls https://get.tuist.io | bash - echo "Checking tuist installation..." - ls -la $HOME/.local/bin/ || echo ".local/bin not found" - find $HOME -name "tuist" 2>/dev/null || echo "tuist binary not found" + echo "Installation complete. Searching for tuist..." + which tuist || echo "tuist not in PATH" + find $HOME -name "tuist" -type f 2>/dev/null | head -5 + ls -la $HOME/.local/bin/ 2>/dev/null || echo ".local/bin not found" + ls -la $HOME/.tuist/ 2>/dev/null || echo ".tuist not found" echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Setup Ruby From e64a2371d768132846936c8fdc3f9475149bfe6e Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Sat, 29 Nov 2025 00:21:35 +0900 Subject: [PATCH 18/23] =?UTF-8?q?mise=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 32 ++++++-------------------------- .github/workflows/deploy.yml | 10 +++------- fastlane/Fastfile | 6 +++--- 3 files changed, 12 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11ed2aa..25a6abc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,16 +20,8 @@ jobs: with: xcode-version: '15.4' - - name: Install Tuist - run: | - echo "Installing Tuist..." - curl -Ls https://get.tuist.io | bash - echo "Installation complete. Searching for tuist..." - which tuist || echo "tuist not in PATH" - find $HOME -name "tuist" -type f 2>/dev/null | head -5 - ls -la $HOME/.local/bin/ 2>/dev/null || echo ".local/bin not found" - ls -la $HOME/.tuist/ 2>/dev/null || echo ".tuist not found" - echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Install mise + uses: jdx/mise-action@v2 - name: Setup Ruby uses: ruby/setup-ruby@v1 @@ -58,9 +50,7 @@ jobs: echo "${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}" | base64 --decode > Oreum/Resources/GoogleService-Info.plist - name: Run Tests - run: | - export PATH="$HOME/.local/bin:$PATH" - bundle exec fastlane test + run: bundle exec fastlane test - name: Upload Test Results if: always() @@ -82,16 +72,8 @@ jobs: with: xcode-version: '15.4' - - name: Install Tuist - run: | - echo "Installing Tuist..." - curl -Ls https://get.tuist.io | bash - echo "Installation complete. Searching for tuist..." - which tuist || echo "tuist not in PATH" - find $HOME -name "tuist" -type f 2>/dev/null | head -5 - ls -la $HOME/.local/bin/ 2>/dev/null || echo ".local/bin not found" - ls -la $HOME/.tuist/ 2>/dev/null || echo ".tuist not found" - echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Install mise + uses: jdx/mise-action@v2 - name: Setup Ruby uses: ruby/setup-ruby@v1 @@ -120,6 +102,4 @@ jobs: echo "${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}" | base64 --decode > Oreum/Resources/GoogleService-Info.plist - name: Build App - run: | - export PATH="$HOME/.local/bin:$PATH" - bundle exec fastlane build + run: bundle exec fastlane build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d104ecf..aaae699 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,10 +21,8 @@ jobs: with: xcode-version: '15.4' - - name: Install Tuist - run: | - curl -Ls https://get.tuist.io | bash - echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Install mise + uses: jdx/mise-action@v2 - name: Setup Ruby uses: ruby/setup-ruby@v1 @@ -89,9 +87,7 @@ jobs: FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} - run: | - export PATH="$HOME/.local/bin:$PATH" - bundle exec fastlane beta + run: bundle exec fastlane beta - name: Clean up keychain if: always() diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 26f007f..d6d4af3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -6,7 +6,7 @@ platform :ios do desc "Run tests" lane :test do # Tuist generate to create Xcode project - sh("cd .. && $HOME/.local/bin/tuist generate") + sh("cd .. && tuist generate") # Run tests run_tests( @@ -21,7 +21,7 @@ platform :ios do desc "Build the app" lane :build do # Tuist generate to create Xcode project - sh("cd .. && $HOME/.local/bin/tuist generate") + sh("cd .. && tuist generate") # Build the app build_app( @@ -36,7 +36,7 @@ platform :ios do desc "Deploy to TestFlight" lane :beta do # Tuist generate to create Xcode project - sh("cd .. && $HOME/.local/bin/tuist generate") + sh("cd .. && tuist generate") # Match for code signing (or use manual signing) # match( From b4680dfeef2e9c70613932fe9c4c73caf41ee7e4 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Sat, 29 Nov 2025 00:24:12 +0900 Subject: [PATCH 19/23] =?UTF-8?q?project=20=ED=8C=8C=EC=9D=BC=20=EC=BB=B4?= =?UTF-8?q?=EB=A7=88=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Project.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.swift b/Project.swift index 6ed717a..1babfa9 100644 --- a/Project.swift +++ b/Project.swift @@ -38,7 +38,7 @@ let project = Project( "NSHealthShareUsageDescription": "등산 기록을 위해 걸음 수와 이동 거리 데이터를 사용합니다.", "NSHealthUpdateUsageDescription": "등산 활동 데이터를 저장하기 위해 HealthKit 접근이 필요합니다.", "NSLocationWhenInUseUsageDescription": "내 주위 명산을 표기하기 위해 위치 정보를 사용합니다." - ], + ] ), sources: ["Oreum/Sources/**"], resources: ["Oreum/Resources/**"], From e13003cb0bda96edb5ffb5fe746ee0fe6b7e17e0 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Sat, 29 Nov 2025 00:26:25 +0900 Subject: [PATCH 20/23] =?UTF-8?q?tuist=20install=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastlane/Fastfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d6d4af3..41a495a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -5,8 +5,8 @@ default_platform(:ios) platform :ios do desc "Run tests" lane :test do - # Tuist generate to create Xcode project - sh("cd .. && tuist generate") + # Tuist install and generate to create Xcode project + sh("cd .. && tuist install && tuist generate") # Run tests run_tests( @@ -20,8 +20,8 @@ platform :ios do desc "Build the app" lane :build do - # Tuist generate to create Xcode project - sh("cd .. && tuist generate") + # Tuist install and generate to create Xcode project + sh("cd .. && tuist install && tuist generate") # Build the app build_app( @@ -35,8 +35,8 @@ platform :ios do desc "Deploy to TestFlight" lane :beta do - # Tuist generate to create Xcode project - sh("cd .. && tuist generate") + # Tuist install and generate to create Xcode project + sh("cd .. && tuist install && tuist generate") # Match for code signing (or use manual signing) # match( From c9130a2c18150c45180e4f1d50c9e2d49021abc0 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Sat, 29 Nov 2025 00:40:10 +0900 Subject: [PATCH 21/23] =?UTF-8?q?build=20verification=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastlane/Fastfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 41a495a..046561c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -23,13 +23,15 @@ platform :ios do # Tuist install and generate to create Xcode project sh("cd .. && tuist install && tuist generate") - # Build the app - build_app( + # Build the app for verification + xcodebuild( workspace: "Oreum.xcworkspace", scheme: "Oreum", + configuration: "Debug", clean: true, - skip_archive: true, - skip_codesigning: true + build: true, + destination: "generic/platform=iOS Simulator", + xcargs: "CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO" ) end From 32415ee17e236ce0de59187f2307bac365c4da86 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Sat, 29 Nov 2025 00:45:06 +0900 Subject: [PATCH 22/23] =?UTF-8?q?xcode=20=EB=B2=84=EC=A0=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 4 ++-- .github/workflows/deploy.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25a6abc..c81e113 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '15.4' + xcode-version: '16.2' - name: Install mise uses: jdx/mise-action@v2 @@ -70,7 +70,7 @@ jobs: - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '15.4' + xcode-version: '16.2' - name: Install mise uses: jdx/mise-action@v2 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index aaae699..c075377 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '15.4' + xcode-version: '16.2' - name: Install mise uses: jdx/mise-action@v2 From b7b36ae9ddd275af1cfdb770ca341e5900bd8117 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Sat, 29 Nov 2025 01:05:22 +0900 Subject: [PATCH 23/23] =?UTF-8?q?dummy=20repo=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9E=90=20public=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/Dummy/DummyClimbRecordRepositoryImpl.swift | 2 +- .../Repositories/Dummy/DummyForecastRepositoryImpl.swift | 2 +- .../Repositories/Dummy/DummyTrackActivityRepositoryImpl.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Data/Sources/Repositories/Dummy/DummyClimbRecordRepositoryImpl.swift b/Data/Sources/Repositories/Dummy/DummyClimbRecordRepositoryImpl.swift index 9b91029..b7a2e2c 100644 --- a/Data/Sources/Repositories/Dummy/DummyClimbRecordRepositoryImpl.swift +++ b/Data/Sources/Repositories/Dummy/DummyClimbRecordRepositoryImpl.swift @@ -21,7 +21,7 @@ public final class DummyClimbRecordRepositoryImpl: ClimbRecordRepository { var shouldReturnError = false var useMockData = false - init() {} + public init() {} public func fetch(keyword: String, isOnlyBookmarked: Bool) -> AnyPublisher, Never> { if shouldReturnError { diff --git a/Data/Sources/Repositories/Dummy/DummyForecastRepositoryImpl.swift b/Data/Sources/Repositories/Dummy/DummyForecastRepositoryImpl.swift index ff505cb..195a4f5 100644 --- a/Data/Sources/Repositories/Dummy/DummyForecastRepositoryImpl.swift +++ b/Data/Sources/Repositories/Dummy/DummyForecastRepositoryImpl.swift @@ -18,7 +18,7 @@ public final class DummyForecastRepositoryImpl: ForecastRepository { var lastNx: Int? var lastNy: Int? - init() {} + public init() {} public func fetchShortTermForecast(nx: Int, ny: Int) -> AnyPublisher, Never> { lastNx = nx diff --git a/Data/Sources/Repositories/Dummy/DummyTrackActivityRepositoryImpl.swift b/Data/Sources/Repositories/Dummy/DummyTrackActivityRepositoryImpl.swift index 93331c9..aa4be05 100644 --- a/Data/Sources/Repositories/Dummy/DummyTrackActivityRepositoryImpl.swift +++ b/Data/Sources/Repositories/Dummy/DummyTrackActivityRepositoryImpl.swift @@ -23,7 +23,7 @@ public final class DummyTrackActivityRepositoryImpl: TrackActivityRepository { var shouldReturnError = false var useMockData = false - init() {} + public init() {} public func requestAuthorization() -> AnyPublisher, Never> { return Just(.success(true))