Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
dc1a4db
refactor: #101 기존 레포지토리 임시 폴더링 수정
dev-domo Feb 3, 2026
64faf0e
feat: #101 데이터베이스 스키마 설정
dev-domo Feb 3, 2026
6e63073
refactor: #101 소셜 로그인, 로그아웃 기능 삭제
dev-domo Feb 11, 2026
409bbdb
refactor: #101 프레임워크명 명시
dev-domo Feb 11, 2026
a0ed942
feat: #101 CoreData 사용 설정
dev-domo Feb 11, 2026
2dbe90a
feat: #101 Member API를 CoreData로 대체
dev-domo Feb 11, 2026
edb4d34
test: #101 Member 관련 유스케이스 테스트 작성
dev-domo Feb 11, 2026
62f6a1c
test: #101 TermsStorage 기능 테스트
dev-domo Feb 25, 2026
9a551d4
refactor: #101 테스트 환경에서 앱과 동일한 모델 인스턴스를 재사용하도록 수정
dev-domo Feb 25, 2026
152d4e9
feat: MemberStorage 구현 및 테스트
dev-domo Mar 4, 2026
7110af7
refactor: #101 생성자 내 객체 생성 코드 수정
dev-domo Mar 4, 2026
6a98f52
refactor: #101 구조체 네이밍 수정
dev-domo Mar 4, 2026
19b5db2
chore: #101 테스트 메서드명 수정
dev-domo Mar 4, 2026
07b6237
refactor: #101 NotificationsEntity 속성명 수정
dev-domo Mar 4, 2026
8b0166d
refactor: #101 엔티티 id를 Int64형으로 수정
dev-domo Mar 11, 2026
b9fa9ef
refactor: #101 파라미터명 수정사항 반영
dev-domo Mar 11, 2026
d80de5b
test: #101 시나리오 엔티티 관련 테스트 작성
dev-domo Mar 11, 2026
e3f4566
test: #101 미션 관련 유스케이스 테스트 작성
dev-domo Mar 11, 2026
16884eb
feat: #101 Mission 저장소 구현
dev-domo Mar 11, 2026
8cb633a
feat: #101 Scenario 저장소 구현
dev-domo Mar 11, 2026
a984b66
feat: #101 Terms 저장소 구현
dev-domo Mar 11, 2026
0ae9d36
chore: #101 불필요한 열거형 제거
dev-domo Mar 11, 2026
f5d3ab0
feat: #101 엔티티 id 배정 유틸리티 구현
dev-domo Mar 11, 2026
737a360
refactor: #101 toRawvalues 메서드 구현
dev-domo Mar 11, 2026
bb4346f
refactor: #101 저장소 구현에 필요한 에러 케이스 정의
dev-domo Mar 11, 2026
0e8ec30
refactor: #101 DefaultValue 재설정
dev-domo Mar 12, 2026
87931e2
fix: #101 CI 빌드 실패 수정
dev-domo Sep 13, 2026
49f450b
fix: CI 러너의 Xcode 버전 드리프트 대응
dev-domo Sep 13, 2026
bf1893d
fix: fastlane scan의 xcodebuild 타임아웃 상향
dev-domo Sep 13, 2026
eb23ef1
fix: fastlane scan 시뮬레이터 기종 드리프트 대응
dev-domo Sep 13, 2026
6c600e5
fix: fastlane 2.239.0으로 업데이트, UTF-8 로케일 강제 지정
dev-domo Sep 13, 2026
22d68ea
fix: bundler 4.x에서 제거된 --deployment 플래그 대응
dev-domo Sep 13, 2026
9a4acd2
chore: SONAR_TOKEN 만료로 SonarCloud Scan을 임시로 non-blocking 처리
dev-domo Sep 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/validate-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 16.4.0
xcode-version: latest-stable

- name: Set up Ruby
uses: ruby/setup-ruby@v1
Expand All @@ -53,20 +53,32 @@ jobs:
${{ runner.os }}-gems-

- name: Bundle Install
run: bundle install --deployment --jobs 4 --retry 3 --path vendor/bundle
run: |
bundle config set --local deployment true
bundle config set --local path vendor/bundle
bundle install --jobs 4 --retry 3

- name: List available devices
run: xcrun simctl list devices

- name: Run Fastlane
run: bundle exec fastlane test
env:
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120
FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 5
LANG: en_US.UTF-8
LANGUAGE: en_US.UTF-8
LC_ALL: en_US.UTF-8

- name: Generate Coverage Report
run: |
xcrun xccov view --report --json BeforeGoing.xcresult > coverage.json
python3 scripts/convert_coverage.py coverage.json sonarqube-generic-coverage.xml

- name: SonarCloud Scan
# TODO: SONAR_TOKEN이 만료/무효 상태로 보임 (2026-09-13 기준). 토큰을 갱신한 뒤
# continue-on-error를 제거해 다시 필수 게이트로 되돌릴 것.
continue-on-error: true
uses: SonarSource/sonarqube-scan-action@v5.0.0
with:
projectBaseDir: .
Expand Down
9 changes: 9 additions & 0 deletions BeforeGoing.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
1827AF552F307CD200D18376 /* Exceptions for "BeforeGoing" folder in "BeforeGoingTests" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Data/BeforeGoingModel.xcdatamodel,
Data/Persistence/CoreData/CoreDataStack.swift,
);
target = 186E06F02DDDF10600E32490 /* BeforeGoingTests */;
};
186E07032DDDF10600E32490 /* Exceptions for "BeforeGoing" folder in "BeforeGoing" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Expand All @@ -45,6 +53,7 @@
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
186E07032DDDF10600E32490 /* Exceptions for "BeforeGoing" folder in "BeforeGoing" target */,
1827AF552F307CD200D18376 /* Exceptions for "BeforeGoing" folder in "BeforeGoingTests" target */,
);
path = BeforeGoing;
sourceTree = "<group>";
Expand Down
9 changes: 9 additions & 0 deletions BeforeGoing/Core/BeforeGoingError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,13 @@ enum BeforeGoingError: Error, Equatable {
case tooManyRequset
case withdrawFailed
case notFoundProvider


case userIDNotFound
case memberNotFound
case termsNotFound
case scenarioNotFound
case notificationNotFound
case invalidMissionType
case invalidDateFormat
}
64 changes: 64 additions & 0 deletions BeforeGoing/Data/BeforeGoingModel.xcdatamodel/contents
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24D60" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Member" representedClassName="Member" syncable="YES" codeGenerationType="class">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="nickname" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="scenarios" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Scenario" inverseName="member" inverseEntity="Scenario"/>
<relationship name="term" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Terms" inverseName="member" inverseEntity="Terms"/>
</entity>
<entity name="Mission" representedClassName="Mission" syncable="YES" codeGenerationType="class">
<attribute name="content" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isChecked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="missionOrder" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="missionType" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="useDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="children" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Mission" inverseName="parent" inverseEntity="Mission"/>
<relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Mission" inverseName="children" inverseEntity="Mission"/>
<relationship name="scenario" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Scenario" inverseName="missions" inverseEntity="Scenario"/>
</entity>
<entity name="Notification" representedClassName="Notification" syncable="YES" codeGenerationType="class">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="daysOfWeek" optional="YES" attributeType="String"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="notificationMethodType" optional="YES" attributeType="String"/>
<attribute name="notificationType" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="scenario" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Scenario" inverseName="notification" inverseEntity="Scenario"/>
<relationship name="timeNotifications" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TimeNotification" inverseName="notification" inverseEntity="TimeNotification"/>
</entity>
<entity name="Scenario" representedClassName="Scenario" syncable="YES" codeGenerationType="class">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="memo" optional="YES" attributeType="String"/>
<attribute name="scenarioName" attributeType="String"/>
<attribute name="scenarioOrder" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="member" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Member" inverseName="scenarios" inverseEntity="Member"/>
<relationship name="missions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Mission" inverseName="scenario" inverseEntity="Mission"/>
<relationship name="notification" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Notification" inverseName="scenario" inverseEntity="Notification"/>
</entity>
<entity name="Terms" representedClassName="Terms" syncable="YES" codeGenerationType="class">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="eventPushAgreed" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isOverFourteen" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="privacyPolicyAgreed" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="termsOfServiceAgreed" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="member" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Member" inverseName="term" inverseEntity="Member"/>
</entity>
<entity name="TimeNotification" representedClassName="TimeNotification" syncable="YES" codeGenerationType="class">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="startHour" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="startMinute" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="notification" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Notification" inverseName="timeNotifications" inverseEntity="Notification"/>
</entity>
</model>
22 changes: 9 additions & 13 deletions BeforeGoing/Data/DataDependencyAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,7 @@ struct DataDependencyAssembler: DependencyAssembler {
DIContainer.shared.register(updateNicknameRequestMapper)

DIContainer.shared.register(type: AuthInterface.self) { _ in
AuthRepository(
networkService: networkService,
tokenReissuer: tokenReissuer,
keyChainService: keyChainService,
userDefaultsService: userDefaultsService,
nonceRequestMapper: nonceRequestMapper,
loginRequestMapper: loginRequestMapper,
tokenValidator: tokenValidator
)
AuthRepository(userDefaultsService: userDefaultsService)
}
DIContainer.shared.register(type: TermsInterface.self) { _ in
TermsRepository(
Expand All @@ -55,11 +47,15 @@ struct DataDependencyAssembler: DependencyAssembler {
)
}
DIContainer.shared.register(type: MemberInterface.self) { _ in
MemberRepository(
networkService: networkService,
keyChainService: keyChainService,
// MemberRepository(
// networkService: networkService,
// keyChainService: keyChainService,
// userDefaultsService: userDefaultsService,
// updateNicknameRequestMapper: updateNicknameRequestMapper
// )
MemberStorage(
userDefaultsService: userDefaultsService,
updateNicknameRequestMapper: updateNicknameRequestMapper
context: CoreDataStack.shared.context
)
}
DIContainer.shared.register(type: ScenarioInterface.self) { _ in
Expand Down
2 changes: 1 addition & 1 deletion BeforeGoing/Data/Model/Notification/NotificationsDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ struct ScenarioNotificationDTO: Decodable {
extension NotificationsDTO {
func toEntity() -> NotificationsEntity {
let scenarios = scenarios.map { $0.toEntity() }
return .init(scenarios: scenarios)
return .init(notifications: scenarios)
}
}

Expand Down
4 changes: 2 additions & 2 deletions BeforeGoing/Data/Network/Extensions/Notification.Name+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@

import Foundation

extension Notification.Name {
static let loginExpired = Notification.Name("loginExpired")
extension Foundation.Notification.Name {
static let loginExpired = Foundation.Notification.Name("loginExpired")
}
51 changes: 51 additions & 0 deletions BeforeGoing/Data/Persistence/CoreData/CoreDataStack.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// CoreDataStack.swift
// BeforeGoing
//
// Created by APPLE on 2/3/26.
//

import CoreData

final class CoreDataStack {

static let shared = CoreDataStack()
private init() {}

// 모델을 static으로 분리해서 외부에서 접근 가능하게
static let managedObjectModel: NSManagedObjectModel = {
let modelName = "BeforeGoingModel"
let uniqueBundles: [Bundle] = Bundle.allBundles
.compactMap { bundle -> (Bundle, URL)? in
guard let url = bundle.url(forResource: modelName, withExtension: "mom") else {
return nil
}
return (bundle, url)
}
.reduce(into: [(Bundle, URL)]()) { result, pair in
if !result.contains(where: { $0.1.path == pair.1.path }) {
result.append(pair)
}
}
.map { $0.0 }

guard let model = NSManagedObjectModel.mergedModel(from: uniqueBundles) else {
fatalError("NSManagedObjectModel을 로드할 수 없습니다.")
}
return model
}()

// 기존 container도 동일한 모델 인스턴스 사용
private let container: NSPersistentContainer = {
let container = NSPersistentContainer(
name: "BeforeGoingModel",
managedObjectModel: CoreDataStack.managedObjectModel
)
container.loadPersistentStores { _, error in
if let error { fatalError("Failed to load store: \(error)") }
}
return container
}()

var context: NSManagedObjectContext { container.viewContext }
}
1 change: 1 addition & 0 deletions BeforeGoing/Data/Persistence/Service/UserDefaultsKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

enum UserDefaultsKey: String, CaseIterable {
case userID
case provider
case lastProvider
}
25 changes: 25 additions & 0 deletions BeforeGoing/Data/Persistence/Service/UserDefaultsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,28 @@ struct UserDefaultsService: UserDefaultsProtocol {
return UserDefaults.standard.value(forKey: key.rawValue) == nil
}
}

final class MockUserDefaultsService: UserDefaultsProtocol {

private var storage: [UserDefaultsKey: Any] = [:]

func save(_ value: Any, key: UserDefaultsKey) -> Bool {
storage[key] = value
return true
}

func load<T>(key: UserDefaultsKey) -> T? {
storage[key] as? T
}

func delete(key: UserDefaultsKey) -> Bool {
guard let _ = storage.removeValue(forKey: key) else {
return false
}
return true
}

func deleteAll() {
storage.removeAll()
}
}
Loading
Loading