From f78f4f00baacc872ec94c886c7c901d867b86604 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:24:30 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[#452]=20=ED=95=B5=EC=8B=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=9C=A0=EB=8B=9B=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?29=EA=B1=B4=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20PR=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20CI=20=EA=B5=AC?= =?UTF-8?q?=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/continuous-integration.yml | 53 ++++---- .../Data/Like/PartnershipLikeManager.swift | 27 ++-- EATSSU/Project.swift | 9 +- .../AcademicNameLocalizationTests.swift | 71 +++++++++++ .../Tests/UnitTests/AppLanguageSandbox.swift | 41 ++++++ .../UnitTests/MealMenuDisplayTests.swift | 74 +++++++++++ .../UnitTests/PartnershipLikeLogicTests.swift | 117 ++++++++++++++++++ EATSSU/Tests/UnitTests/SampleTests.swift | 33 ----- .../UnitTests/StoreCategoryMappingTests.swift | 77 ++++++++++++ 9 files changed, 438 insertions(+), 64 deletions(-) create mode 100644 EATSSU/Tests/UnitTests/AcademicNameLocalizationTests.swift create mode 100644 EATSSU/Tests/UnitTests/AppLanguageSandbox.swift create mode 100644 EATSSU/Tests/UnitTests/MealMenuDisplayTests.swift create mode 100644 EATSSU/Tests/UnitTests/PartnershipLikeLogicTests.swift delete mode 100644 EATSSU/Tests/UnitTests/SampleTests.swift create mode 100644 EATSSU/Tests/UnitTests/StoreCategoryMappingTests.swift diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 908a21c9..0975fe67 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -3,41 +3,50 @@ name: Continuous Integration on: pull_request: branches: - - qa + - develop jobs: - Continuous_Integration: + unit-tests: + name: Unit Tests runs-on: macos-latest environment: Configuration Files steps: - uses: actions/checkout@v4 - - uses: jdx/mise-action@v2 - - name: Install Tuist - run: | - bash <(curl -Ls https://install.tuist.io) + # .mise.toml에 고정된 tuist 설치 + - uses: jdx/mise-action@v2 - - name: Create Release & Debug Configuration Files + - name: Create secret xcconfig files env: BASE_URL: ${{ secrets.BASE_URL }} KAKAO_API_KEY: ${{ secrets.KAKAO_API_KEY }} run: | - cd EATSSU_MVC/EATSSU_MVC/Resources - mkdir Secrets - cd Secrets + mkdir -p EATSSU/App/Resources/Secrets + for config in Release Debug; do + { + echo "BASE_URL = $BASE_URL" + echo "KAKAO_API_KEY = $KAKAO_API_KEY" + } > "EATSSU/App/Resources/Secrets/$config.xcconfig" + done - echo "BASE_URL = $BASE_URL" >> Release.xcconfig - echo "KAKAO_API_KEY = $KAKAO_API_KEY" >> Release.xcconfig - cat Release.xcconfig + - name: Install dependencies + run: tuist install - echo "BASE_URL = $BASE_URL" >> Debug.xcconfig - echo "KAKAO_API_KEY = $KAKAO_API_KEY" >> Debug.xcconfig - cat Debug.xcconfig + - name: Generate project + run: tuist generate --no-open - - name: Install Dependencies - run: | - tuist install - - - name: Build Project + - name: Run unit tests run: | - tuist build \ No newline at end of file + DEVICE_ID=$(xcrun simctl list devices available | grep -oE '[0-9A-F-]{36}' | head -1) + echo "Using simulator $DEVICE_ID" + set -o pipefail + xcodebuild test \ + -workspace EATSSU_WORKSPACE.xcworkspace \ + -scheme EATSSUUnitTests \ + -destination "platform=iOS Simulator,id=$DEVICE_ID" \ + CODE_SIGNING_ALLOWED=NO | xcbeautify --renderer github-actions || \ + xcodebuild test \ + -workspace EATSSU_WORKSPACE.xcworkspace \ + -scheme EATSSUUnitTests \ + -destination "platform=iOS Simulator,id=$DEVICE_ID" \ + CODE_SIGNING_ALLOWED=NO diff --git a/EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift b/EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift index 7bc9b9ed..e72e7c28 100644 --- a/EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift +++ b/EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift @@ -116,9 +116,7 @@ final class PartnershipLikeManager { /// 항목별 현재 찜 상태와 다른 것만 서버 토글. 목록 확보에 실패한 경우엔 지도 응답의 isLiked를 기준으로 삼는다 private func toggle(liked: Bool, store: PartnershipDTO, completion: @escaping (Result) -> Void) { - let targets = store.partnershipInfos - .filter { currentlyLiked($0) != liked } - .map(\.id) + let targets = Self.toggleTargets(in: store, toBe: liked, isCurrentlyLiked: currentlyLiked) guard !targets.isEmpty else { applyLocalState(liked: liked, store: store) completion(.success(())) @@ -190,8 +188,19 @@ final class PartnershipLikeManager { return hasLoaded ? likedPartnershipIds.contains(info.id) : info.isLiked } + /// 현재 상태와 목표 상태가 다른 항목만 토글 대상 (서버 API가 토글 방식이라 같은 상태를 다시 호출하면 뒤집힌다) + static func toggleTargets( + in store: PartnershipDTO, + toBe liked: Bool, + isCurrentlyLiked: (PartnershipInfoDTO) -> Bool + ) -> [Int] { + store.partnershipInfos + .filter { isCurrentlyLiked($0) != liked } + .map(\.id) + } + /// 찜 목록 응답에서 실제 찜된 항목 id. 플래그가 하나도 없으면(구버전 응답) 소속 항목 전부로 간주 - private static func likedIds(in store: PartnershipDTO) -> [Int] { + static func likedIds(in store: PartnershipDTO) -> [Int] { let flagged = store.partnershipInfos.filter(\.isLiked).map(\.id) return flagged.isEmpty ? store.partnershipIds : flagged } @@ -209,12 +218,16 @@ final class PartnershipLikeManager { persistOrder() } - /// 로컬에 기록된 찜 시각 기준 최근순. 기록이 없는 항목(다른 기기에서 찜 등)은 서버 순서대로 뒤에 둔다 private func sortedByRecent(_ stores: [PartnershipDTO]) -> [PartnershipDTO] { + Self.sortedByRecent(stores, likedAt: likedAtByStoreKey) + } + + /// 로컬에 기록된 찜 시각 기준 최근순. 기록이 없는 항목(다른 기기에서 찜 등)은 서버 순서대로 뒤에 둔다 + static func sortedByRecent(_ stores: [PartnershipDTO], likedAt: [String: Double]) -> [PartnershipDTO] { let indexed = stores.enumerated().map { ($0.offset, $0.element) } return indexed.sorted { lhs, rhs in - let lhsTime = likedAtByStoreKey[lhs.1.storeKey] - let rhsTime = likedAtByStoreKey[rhs.1.storeKey] + let lhsTime = likedAt[lhs.1.storeKey] + let rhsTime = likedAt[rhs.1.storeKey] switch (lhsTime, rhsTime) { case let (l?, r?): return l > r case (_?, nil): return true diff --git a/EATSSU/Project.swift b/EATSSU/Project.swift index 24dc8195..9c12b6c2 100644 --- a/EATSSU/Project.swift +++ b/EATSSU/Project.swift @@ -320,7 +320,12 @@ let project = Project( sources: ["Tests/UnitTests/**"], dependencies: [ .target(name: "EATSSU-DEV"), - ] + ], + settings: .settings(base: [ + // 프로젝트 공통 -all_load/-ObjC가 테스트 번들에 상속되면 호스트 앱의 정적 라이브러리를 + // 강제 로드하다 링크가 깨진다. 테스트 번들은 호스트 앱 심볼을 쓰므로 비워둔다 + "OTHER_LDFLAGS": "" + ]) ), ], schemes: [ @@ -328,7 +333,7 @@ let project = Project( name: "EATSSU-DEV", shared: true, buildAction: .buildAction(targets: [.target("EATSSU-DEV")]), - testAction: .targets(["EATSSU-DEV"]), + testAction: .targets(["EATSSUUnitTests"]), runAction: .runAction(configuration: "Debug"), archiveAction: .archiveAction(configuration: "Release"), profileAction: .profileAction(configuration: "Release"), diff --git a/EATSSU/Tests/UnitTests/AcademicNameLocalizationTests.swift b/EATSSU/Tests/UnitTests/AcademicNameLocalizationTests.swift new file mode 100644 index 00000000..f8578c72 --- /dev/null +++ b/EATSSU/Tests/UnitTests/AcademicNameLocalizationTests.swift @@ -0,0 +1,71 @@ +// +// AcademicNameLocalizationTests.swift +// EATSSUUnitTests +// +// Created by 황상환 on 8/31/26. +// + +import XCTest + +@testable import EATSSU_DEV + +/// 단과대/학과명 번역 키 정규화와 언어별 표기 규칙 검증 +final class AcademicNameLocalizationTests: XCTestCase { + + private var languageSandbox: AppLanguageSandbox! + + override func setUp() { + super.setUp() + languageSandbox = AppLanguageSandbox() + } + + override func tearDown() { + languageSandbox.restore() + languageSandbox = nil + super.tearDown() + } + + // MARK: - normalizedKey (서버 표기 차이 흡수) + + func test_아래아_구분자는_키에서_제거된다() { + XCTAssertEqual(TextLiteral.Academic.normalizedKey("산업 ㆍ 정보시스템공학과"), "산업정보시스템공학과") + } + + func test_가운뎃점과_공백은_키에서_제거된다() { + XCTAssertEqual(TextLiteral.Academic.normalizedKey("정보통계·보험수리학과"), "정보통계보험수리학과") + } + + func test_괄호는_키에서_제거된다() { + XCTAssertEqual(TextLiteral.Academic.normalizedKey("전자정보공학부(전자공학전공)"), "전자정보공학부전자공학전공") + } + + // MARK: - 언어별 표기 + + func test_영어에서_등록된_학과는_영문으로_표기된다() { + languageSandbox.set(.english) + XCTAssertEqual(TextLiteral.Academic.college("인문대학"), "College of Humanities") + XCTAssertEqual(TextLiteral.Academic.department("컴퓨터학부"), "Computer Science & Engineering") + } + + func test_영어에서_서버_표기가_달라도_정규화로_매칭된다() { + languageSandbox.set(.english) + XCTAssertEqual(TextLiteral.Academic.department("정보통계·보험수리학과"), "Actuarial Science") + } + + func test_등록되지_않은_이름은_원본을_그대로_반환한다() { + languageSandbox.set(.english) + XCTAssertEqual(TextLiteral.Academic.department("없는학과"), "없는학과") + } + + func test_빈_문자열은_키를_노출하지_않고_빈_문자열을_반환한다() { + languageSandbox.set(.english) + XCTAssertEqual(TextLiteral.Academic.college(""), "") + } + + func test_한국어는_ko_스트링스_표기를_사용한다() { + languageSandbox.set(.korean) + XCTAssertEqual(TextLiteral.Academic.college("인문대학"), "인문대학") + // 서버가 아래아 표기를 줘도 ko 값(스프레드시트 표기)으로 통일된다 + XCTAssertEqual(TextLiteral.Academic.department("산업 ㆍ 정보시스템공학과"), "산업정보시스템공학과") + } +} diff --git a/EATSSU/Tests/UnitTests/AppLanguageSandbox.swift b/EATSSU/Tests/UnitTests/AppLanguageSandbox.swift new file mode 100644 index 00000000..76e63c1d --- /dev/null +++ b/EATSSU/Tests/UnitTests/AppLanguageSandbox.swift @@ -0,0 +1,41 @@ +// +// AppLanguageSandbox.swift +// EATSSUUnitTests +// +// Created by 황상환 on 8/31/26. +// + +import Foundation + +@testable import EATSSU_DEV + +/// 테스트에서 앱 언어를 바꾸고, 종료 시 원래 UserDefaults 상태로 되돌리는 헬퍼 +/// (AppLanguageManager가 UserDefaults를 직접 읽으므로 테스트 간 오염을 막는다) +final class AppLanguageSandbox { + + private enum Key { + static let selected = "selectedAppLanguage" + static let manual = "didSelectLanguageManually" + } + + private let savedLanguage: String? + private let savedManualFlag: Bool + + init() { + savedLanguage = UserDefaults.standard.string(forKey: Key.selected) + savedManualFlag = UserDefaults.standard.bool(forKey: Key.manual) + } + + func set(_ language: AppLanguage) { + AppLanguageManager.shared.changeLanguage(to: language) + } + + func restore() { + if let savedLanguage { + UserDefaults.standard.set(savedLanguage, forKey: Key.selected) + } else { + UserDefaults.standard.removeObject(forKey: Key.selected) + } + UserDefaults.standard.set(savedManualFlag, forKey: Key.manual) + } +} diff --git a/EATSSU/Tests/UnitTests/MealMenuDisplayTests.swift b/EATSSU/Tests/UnitTests/MealMenuDisplayTests.swift new file mode 100644 index 00000000..2e097555 --- /dev/null +++ b/EATSSU/Tests/UnitTests/MealMenuDisplayTests.swift @@ -0,0 +1,74 @@ +// +// MealMenuDisplayTests.swift +// EATSSUUnitTests +// +// Created by 황상환 on 8/31/26. +// + +import XCTest + +@testable import EATSSU_DEV + +/// 변동식단 대표메뉴 표시 규칙과 언어 파라미터 검증 +final class MealMenuDisplayTests: XCTestCase { + + private var languageSandbox: AppLanguageSandbox! + + override func setUp() { + super.setUp() + languageSandbox = AppLanguageSandbox() + } + + override func tearDown() { + languageSandbox.restore() + languageSandbox = nil + super.tearDown() + } + + private func makeMeal(_ menus: [(name: String, isMain: Bool?)]) -> ChangeMenuTableResponse { + ChangeMenuTableResponse( + mealId: 1, + price: 5000, + rating: nil, + briefMenus: menus.enumerated().map { BriefMenus(menuId: $0.offset, name: $0.element.name, isMain: $0.element.isMain) } + ) + } + + func test_영어에서는_대표메뉴만_표시한다() { + languageSandbox.set(.english) + let meal = makeMeal([("Pork Cutlet", true), ("김치", false), ("밥", false)]) + XCTAssertEqual(meal.displayMenus.map(\.name), ["Pork Cutlet"]) + } + + func test_영어라도_대표메뉴가_없으면_전체를_표시한다() { + // 대표메뉴 데이터가 아직 없는 식단은 전부 isMain=false/nil로 내려온다 (서버 안내) + languageSandbox.set(.english) + let meal = makeMeal([("김치", false), ("밥", nil)]) + XCTAssertEqual(meal.displayMenus.count, 2) + } + + func test_한국어에서는_대표메뉴_여부와_무관하게_전체를_표시한다() { + languageSandbox.set(.korean) + let meal = makeMeal([("돈까스", true), ("김치", false)]) + XCTAssertEqual(meal.displayMenus.count, 2) + } + + func test_변동식단_언어_파라미터는_영어만_전달한다() { + languageSandbox.set(.english) + XCTAssertEqual(ChangeMenuTableResponse.mealLanguageParameter, "EN") + + languageSandbox.set(.japanese) + XCTAssertNil(ChangeMenuTableResponse.mealLanguageParameter) + + languageSandbox.set(.korean) + XCTAssertNil(ChangeMenuTableResponse.mealLanguageParameter) + } + + func test_고정메뉴_언어_파라미터는_한국어_외_전부_전달한다() { + languageSandbox.set(.japanese) + XCTAssertEqual(ChangeMenuTableResponse.fixedMenuLanguageParameter, "JA") + + languageSandbox.set(.korean) + XCTAssertNil(ChangeMenuTableResponse.fixedMenuLanguageParameter) + } +} diff --git a/EATSSU/Tests/UnitTests/PartnershipLikeLogicTests.swift b/EATSSU/Tests/UnitTests/PartnershipLikeLogicTests.swift new file mode 100644 index 00000000..053fd313 --- /dev/null +++ b/EATSSU/Tests/UnitTests/PartnershipLikeLogicTests.swift @@ -0,0 +1,117 @@ +// +// PartnershipLikeLogicTests.swift +// EATSSUUnitTests +// +// Created by 황상환 on 8/31/26. +// + +import XCTest + +@testable import EATSSU_DEV + +/// 제휴 찜 순수 로직 검증 (토글 대상 계산, 찜 목록 해석, 최근 추가순 정렬) +final class PartnershipLikeLogicTests: XCTestCase { + + // MARK: - Helpers + + private func makeInfo(id: Int, isLiked: Bool = false) -> PartnershipInfoDTO { + PartnershipInfoDTO( + id: id, + collegeName: "IT대학", + departmentName: nil, + likeCount: 0, + isLiked: isLiked, + description: "제휴 내용", + startDate: "2026-09-01", + endDate: "2026-12-31", + periodType: .normal + ) + } + + private func makeStore(name: String, infos: [PartnershipInfoDTO]) -> PartnershipDTO { + PartnershipDTO( + storeName: name, + longitude: 126.95, + latitude: 37.49, + restaurantType: "RESTAURANT", + naverMapUrl: nil, + kakaoMapUrl: nil, + partnershipInfos: infos + ) + } + + // MARK: - toggleTargets (토글 API라 같은 상태를 다시 호출하면 뒤집힌다) + + func test_찜_추가시_이미_찜된_항목은_토글하지_않는다() { + let store = makeStore(name: "A", infos: [makeInfo(id: 1), makeInfo(id: 2), makeInfo(id: 3)]) + let alreadyLiked: Set = [2] + + let targets = PartnershipLikeManager.toggleTargets(in: store, toBe: true) { alreadyLiked.contains($0.id) } + + XCTAssertEqual(targets, [1, 3]) + } + + func test_찜_해제시_찜된_항목만_토글한다() { + let store = makeStore(name: "A", infos: [makeInfo(id: 1), makeInfo(id: 2)]) + let alreadyLiked: Set = [1] + + let targets = PartnershipLikeManager.toggleTargets(in: store, toBe: false) { alreadyLiked.contains($0.id) } + + XCTAssertEqual(targets, [1]) + } + + func test_상태가_모두_목표와_같으면_토글_대상이_없다() { + let store = makeStore(name: "A", infos: [makeInfo(id: 1), makeInfo(id: 2)]) + + let targets = PartnershipLikeManager.toggleTargets(in: store, toBe: true) { _ in true } + + XCTAssertTrue(targets.isEmpty) + } + + // MARK: - likedIds (찜 목록 응답 해석) + + func test_isLiked_플래그가_있으면_해당_항목만_찜으로_해석한다() { + let store = makeStore(name: "A", infos: [makeInfo(id: 1, isLiked: true), makeInfo(id: 2, isLiked: false)]) + + XCTAssertEqual(PartnershipLikeManager.likedIds(in: store), [1]) + } + + func test_플래그가_하나도_없으면_전체를_찜으로_간주한다() { + // 구버전 응답 호환: 찜 목록에 온 업체인데 플래그가 없으면 전부 찜으로 본다 + let store = makeStore(name: "A", infos: [makeInfo(id: 1), makeInfo(id: 2)]) + + XCTAssertEqual(PartnershipLikeManager.likedIds(in: store), [1, 2]) + } + + // MARK: - sortedByRecent (최근 추가순) + + func test_찜한_시각이_최근인_업체가_앞에_온다() { + let a = makeStore(name: "A", infos: [makeInfo(id: 1)]) + let b = makeStore(name: "B", infos: [makeInfo(id: 2)]) + let likedAt = [a.storeKey: 100.0, b.storeKey: 200.0] + + let sorted = PartnershipLikeManager.sortedByRecent([a, b], likedAt: likedAt) + + XCTAssertEqual(sorted.map(\.storeName), ["B", "A"]) + } + + func test_시각_기록이_없는_업체는_서버_순서대로_뒤에_온다() { + let recorded = makeStore(name: "기록됨", infos: [makeInfo(id: 1)]) + let unknown1 = makeStore(name: "미기록1", infos: [makeInfo(id: 2)]) + let unknown2 = makeStore(name: "미기록2", infos: [makeInfo(id: 3)]) + let likedAt = [recorded.storeKey: 100.0] + + let sorted = PartnershipLikeManager.sortedByRecent([unknown1, recorded, unknown2], likedAt: likedAt) + + XCTAssertEqual(sorted.map(\.storeName), ["기록됨", "미기록1", "미기록2"]) + } + + // MARK: - storeKey + + func test_업체_키는_이름과_좌표로_구성된다() { + let store = makeStore(name: "현선이네", infos: [makeInfo(id: 1)]) + + XCTAssertEqual(store.storeKey, "현선이네|37.49|126.95") + XCTAssertEqual(store.partnershipIds, [1]) + } +} diff --git a/EATSSU/Tests/UnitTests/SampleTests.swift b/EATSSU/Tests/UnitTests/SampleTests.swift deleted file mode 100644 index 205b1d59..00000000 --- a/EATSSU/Tests/UnitTests/SampleTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// SampleTests.swift -// EATSSUUnitTests -// -// Created by Jiwoong CHOI on 9/14/24. -// - -import XCTest - -final class SampleTests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - measure { - // Put the code you want to measure the time of here. - } - } -} diff --git a/EATSSU/Tests/UnitTests/StoreCategoryMappingTests.swift b/EATSSU/Tests/UnitTests/StoreCategoryMappingTests.swift new file mode 100644 index 00000000..d4877a6b --- /dev/null +++ b/EATSSU/Tests/UnitTests/StoreCategoryMappingTests.swift @@ -0,0 +1,77 @@ +// +// StoreCategoryMappingTests.swift +// EATSSUUnitTests +// +// Created by 황상환 on 8/31/26. +// + +import XCTest + +@testable import EATSSU_DEV + +/// 식당/업종 enum과 서버 문자열 매핑 검증 +final class StoreCategoryMappingTests: XCTestCase { + + private var languageSandbox: AppLanguageSandbox! + + override func setUp() { + super.setUp() + languageSandbox = AppLanguageSandbox() + } + + override func tearDown() { + languageSandbox.restore() + languageSandbox = nil + super.tearDown() + } + + // MARK: - Restaurant + + func test_식당_rawValue는_strings_키로_사용된다() { + XCTAssertEqual(Restaurant.dodamRestaurant.rawValue, "dodamRestaurant") + XCTAssertEqual(Restaurant.snackCorner.rawValue, "snackCorner") + } + + func test_식당_서버_식별자_매핑() { + XCTAssertEqual(Restaurant.studentRestaurant.identifier, "HAKSIK") + XCTAssertEqual(Restaurant.snackCorner.identifier, "SNACK_CORNER") + } + + func test_한국어_표시_이름으로_식당을_역조회한다() { + languageSandbox.set(.korean) + XCTAssertEqual(Restaurant(title: "도담 식당"), .dodamRestaurant) + XCTAssertNil(Restaurant(title: "없는 식당")) + } + + func test_RemoteConfig_매칭용_한국어_이름() { + XCTAssertEqual(Restaurant.facultyRestaurant.koreanName, "FACULTY (교직원 전용)") + } + + // MARK: - GoodPriceCategory + + func test_서버_카테고리_문자열_역매핑() { + XCTAssertEqual(GoodPriceCategory(serverValue: "BAKERY"), .bakery) + XCTAssertNil(GoodPriceCategory(serverValue: "UNKNOWN")) + } + + func test_전체는_서버_파라미터를_보내지_않는다() { + XCTAssertNil(GoodPriceCategory.all.serverValue) + } + + func test_전체를_제외한_카테고리는_서버_값과_왕복된다() { + for category in GoodPriceCategory.allCases where category != .all { + let serverValue = try! XCTUnwrap(category.serverValue) + XCTAssertEqual(GoodPriceCategory(serverValue: serverValue), category) + } + } + + // MARK: - PartnershipFilter + + func test_제휴_필터의_서버_업종_값() { + XCTAssertEqual(PartnershipFilter.restaurant.restaurantType, "RESTAURANT") + XCTAssertEqual(PartnershipFilter.cafe.restaurantType, "CAFE") + XCTAssertEqual(PartnershipFilter.pub.restaurantType, "PUB") + XCTAssertNil(PartnershipFilter.all.restaurantType) + XCTAssertNil(PartnershipFilter.festival.restaurantType) + } +} From 51991db9015f2632ab6ec1249bd7935ed7532817 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:42:43 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[#452]=20CI=20=EC=8B=9C=EB=AE=AC=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=84=A0=ED=83=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95=20(=EC=82=AC=EC=9A=A9=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=20=EA=B8=B0=EA=B8=B0=20=EC=97=86=EC=9C=BC=EB=A9=B4=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/continuous-integration.yml | 31 ++++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 0975fe67..b69bf4bf 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -35,18 +35,31 @@ jobs: - name: Generate project run: tuist generate --no-open - - name: Run unit tests + - name: Select or create simulator + id: simulator run: | - DEVICE_ID=$(xcrun simctl list devices available | grep -oE '[0-9A-F-]{36}' | head -1) + xcrun simctl list runtimes + DEVICE_ID=$(xcrun simctl list devices available -j | python3 -c ' + import json, sys + data = json.load(sys.stdin)["devices"] + for runtime in sorted(data, reverse=True): + for device in data[runtime]: + if device.get("isAvailable") and "iPhone" in device.get("name", ""): + print(device["udid"]); sys.exit(0) + ') + if [ -z "$DEVICE_ID" ]; then + echo "No available simulator, creating one" + RUNTIME=$(xcrun simctl list runtimes -j | python3 -c 'import json,sys; rts=[r for r in json.load(sys.stdin)["runtimes"] if r["platform"]=="iOS" and r["isAvailable"]]; print(rts[-1]["identifier"])') + DEVICE_ID=$(xcrun simctl create "ci-iphone" "iPhone 16" "$RUNTIME") + fi echo "Using simulator $DEVICE_ID" + echo "device_id=$DEVICE_ID" >> "$GITHUB_OUTPUT" + + - name: Run unit tests + run: | set -o pipefail xcodebuild test \ -workspace EATSSU_WORKSPACE.xcworkspace \ -scheme EATSSUUnitTests \ - -destination "platform=iOS Simulator,id=$DEVICE_ID" \ - CODE_SIGNING_ALLOWED=NO | xcbeautify --renderer github-actions || \ - xcodebuild test \ - -workspace EATSSU_WORKSPACE.xcworkspace \ - -scheme EATSSUUnitTests \ - -destination "platform=iOS Simulator,id=$DEVICE_ID" \ - CODE_SIGNING_ALLOWED=NO + -destination "platform=iOS Simulator,id=${{ steps.simulator.outputs.device_id }}" \ + CODE_SIGNING_ALLOWED=NO 2>&1 | tail -100 From 59c87c58d6c746c3328e5dd0ff7b9a02a837dd23 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:48:43 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[#452]=20CI=20Xcode=EB=A5=BC=20=EB=A1=9C?= =?UTF-8?q?=EC=BB=AC=EA=B3=BC=20=EB=8F=99=EC=9D=BC=ED=95=9C=2026.1?= =?UTF-8?q?=EB=A1=9C=20=EA=B3=A0=EC=A0=95=20(=EC=B5=9C=EC=8B=A0=20SDK?= =?UTF-8?q?=EC=97=90=EC=84=9C=20realm-core=20=EC=BB=B4=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EB=B6=88=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/continuous-integration.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b69bf4bf..c8fb1da5 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -16,6 +16,13 @@ jobs: # .mise.toml에 고정된 tuist 설치 - uses: jdx/mise-action@v2 + # 러너 기본 Xcode(최신)에서는 고정된 realm-core가 컴파일되지 않아 로컬과 같은 26.1로 고정 + - name: Select Xcode + run: | + ls -d /Applications/Xcode*.app + sudo xcode-select -s /Applications/Xcode_26.1.app + xcodebuild -version + - name: Create secret xcconfig files env: BASE_URL: ${{ secrets.BASE_URL }} From 6878bff290f7778793c8ddca5c8e357805396350 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:53:32 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[#452]=20CI=EC=97=90=EC=84=9C=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20Xcode=EC=9A=A9=20iOS=20=EC=8B=9C=EB=AE=AC=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=9F=B0=ED=83=80=EC=9E=84=20=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=EB=A1=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/continuous-integration.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index c8fb1da5..b8cc3881 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -23,6 +23,10 @@ jobs: sudo xcode-select -s /Applications/Xcode_26.1.app xcodebuild -version + # 러너 이미지에는 기본 Xcode용 런타임만 있어, 고정한 Xcode에 맞는 iOS 시뮬레이터 런타임을 내려받는다 + - name: Download iOS simulator runtime + run: sudo xcodebuild -downloadPlatform iOS + - name: Create secret xcconfig files env: BASE_URL: ${{ secrets.BASE_URL }} From 0dee0d793ee3f507086225355f09f66212b2016d Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:13:05 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[#452]=20CI=EC=97=90=EC=84=9C=20GoogleServi?= =?UTF-8?q?ce-Info.plist=20=EC=8B=9C=ED=81=AC=EB=A6=BF=20=EB=B3=B5?= =?UTF-8?q?=EC=9B=90=20(=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=98=B8=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=95=B1=20=EC=8B=A4=ED=96=89=EC=97=90=20=ED=95=84?= =?UTF-8?q?=EC=9A=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/continuous-integration.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b8cc3881..57ae584c 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -40,6 +40,11 @@ jobs: } > "EATSSU/App/Resources/Secrets/$config.xcconfig" done + - name: Create GoogleService-Info.plist + env: + GOOGLE_SERVICE_INFO_PLIST: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST }} + run: echo "$GOOGLE_SERVICE_INFO_PLIST" | base64 --decode > EATSSU/App/Resources/GoogleService-Info.plist + - name: Install dependencies run: tuist install From e7fc0429ad6a0db545ee2a5f9d85ad067e6b6632 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:36:15 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[#452]=20CI=20xcconfig=EC=97=90=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=EB=90=9C=20=EC=8B=9C=ED=81=AC=EB=A6=BF=205=EC=A2=85?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(AppGroupID=20=EB=93=B1=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=ED=95=84=EC=88=98=20=EA=B0=92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/continuous-integration.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 57ae584c..e8480d34 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -31,12 +31,22 @@ jobs: env: BASE_URL: ${{ secrets.BASE_URL }} KAKAO_API_KEY: ${{ secrets.KAKAO_API_KEY }} + KAKAO_REST_API_KEY: ${{ secrets.KAKAO_REST_API_KEY }} + NAVER_CLIENT_ID: ${{ secrets.NAVER_CLIENT_ID }} + HOLIDAY_API_KEY: ${{ secrets.HOLIDAY_API_KEY }} + APP_GROUP_ID: ${{ secrets.APP_GROUP_ID }} + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} run: | mkdir -p EATSSU/App/Resources/Secrets for config in Release Debug; do { echo "BASE_URL = $BASE_URL" echo "KAKAO_API_KEY = $KAKAO_API_KEY" + echo "KAKAO_REST_API_KEY = $KAKAO_REST_API_KEY" + echo "NAVER_CLIENT_ID = $NAVER_CLIENT_ID" + echo "HOLIDAY_API_KEY = $HOLIDAY_API_KEY" + echo "APP_GROUP_ID = $APP_GROUP_ID" + echo "POSTHOG_API_KEY = $POSTHOG_API_KEY" } > "EATSSU/App/Resources/Secrets/$config.xcconfig" done