Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d00d32c
feat: #33 마이페이지 알림 설정 화면 구현
kangddong Mar 27, 2026
bc3ee6e
feat: FCM 토큰 등록/삭제 API 연동 및 Firebase 설정 추가
kangddong Mar 27, 2026
42ebf70
chore: #42 빌드 환경 분리 및 푸시 entitlements 구성
kangddong Apr 28, 2026
e902565
feat: #42 FCM/APNs 푸시 알림 부트스트랩 연결
kangddong Apr 28, 2026
d179a15
feat: #42 FCM 푸시 테스트 엔드포인트 추가
kangddong Apr 28, 2026
d3f9f6f
chore: #42 FCM 푸시 테스트 페이지 및 UITests 추가
kangddong Apr 28, 2026
d61b92c
feat: #42 알림 설정 API 엔드포인트/응답 모델 추가
kangddong Apr 28, 2026
c6ef65e
feat: #42 알림 설정 화면 서버 연동 및 기기 알림 배너 추가
kangddong Apr 28, 2026
f19adce
feat: #42 도리 등록 후 시스템 알림 권한 안내 알럿
kangddong Apr 28, 2026
692da2b
chore: #42 마이페이지에 FCM 푸시 테스트 진입점 연결
kangddong Apr 28, 2026
31a6f1d
chore: #42 릴리스 빌드 앱 표시명을 한국어로 처리
kangddong Apr 28, 2026
469c4e2
fix: #42 전체 푸시 토글 상태에 따른 설명 문구 동적 처리
kangddong Apr 29, 2026
fb86203
chore: #42 앱 버전 1.1.0으로 올리고 Info.plist에 버전 변수 노출
kangddong Apr 29, 2026
3ecf539
chore: #42 타겟 destination을 iPhone으로 한정하고 세로 방향 고정
kangddong Apr 29, 2026
353ea2a
fix: #42 Info.plist 딕셔너리 리터럴을 단계적으로 빌드해 Swift 6 중복 키 트랩 회피
kangddong Apr 29, 2026
c5c4a6a
Merge branch 'develop' into feat/42-we-need-notification
kangddong Apr 29, 2026
b27b3df
fix: #42 develop 머지로 깨진 InfoPlist 파일 복구 및 orientation 분리 머지
kangddong Apr 29, 2026
e1bb3ac
test: #42 CI Xcode 버전 고정 가설 검증
kangddong May 6, 2026
3809163
revert: #42 검증 후 Xcode latest-stable + mutation 우회 코드로 복원
kangddong May 6, 2026
5938e0d
test: #42 CI 실패 재현 — literal 13키 + Xcode latest-stable 조합
kangddong May 6, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ Derived/
*.xcodeproj
*.xcworkspace
.claude/worktrees
GoogleService-Info.plist
8 changes: 8 additions & 0 deletions Projects/App/DoriAppDebug.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
16 changes: 15 additions & 1 deletion Projects/App/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ let project = Project.dori(
.app(
name: "DoriApp",
bundleId: Environment.App.baseBundleId,
resources: [.glob(pattern: "Resources/**", excluding: ["Resources/info.plist"])],
resources: [.glob(pattern: "Resources/**", excluding: ["Resources/info.plist", "Resources/*.entitlements"])],
dependencies: [
DoriModules.onboarding.module.projectDependency,
DoriModules.addDori.module.projectDependency,
Expand All @@ -24,10 +24,24 @@ let project = Project.dori(
DoriModules.networkImpl.module.projectDependency,
DoriModules.kakaoAuth.module.projectDependency,
DoriModules.keychain.module.projectDependency,
DoriModules.fcm.module.projectDependency,
DoriModules.designSystem.module.projectDependency,
DoriModules.core.module.projectDependency,
.external(.composableArchitecture)
],
entitlements: .file(path: "Resources/DoriApp.entitlements")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앱 타겟이 항상 Resources/DoriApp.entitlements를 사용하고, 해당 파일의 aps-environmentdevelopment로 고정되어 있습니다. Release/App Store 배포에서는 production APNs entitlement가 필요하므로 배포 서명 실패나 푸시 환경 불일치가 날 수 있습니다. Debug/Release 별 entitlements를 분리하거나 빌드 설정으로 aps-environment를 맞춰주세요.

),
.target(
name: "DoriAppUITests",
destinations: [.iPhone],
product: .uiTests,
bundleId: "\(Environment.App.baseBundleId).UITests",
deploymentTargets: .iOS(Environment.deploymentTarget),
sources: ["UITests/**"],
dependencies: [
.target(name: "DoriApp")
],
settings: .testSettings
),
]
)
8 changes: 8 additions & 0 deletions Projects/App/Resources/DoriApp.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
38 changes: 38 additions & 0 deletions Projects/App/Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// AppDelegate.swift
// Dori-iOS
//
// Created by 강동영 on 3/25/26.
//

import UIKit
import PlatformFCM

final class AppDelegate: NSObject, UIApplicationDelegate {

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
FCMService.shared.configure()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FirebaseApp.configure()가 런치 시 무조건 실행되는데, GoogleService-Info.plist는 PR 트리에 없고 .gitignore에 추가되어 있습니다. 로컬에는 ignored 파일이 있어서 통과해도 fresh clone/CI/TestFlight 환경에서는 번들에 설정 파일이 없을 수 있고, 그 경우 앱 시작 단계에서 Firebase 초기화가 실패합니다. 환경별 plist를 빌드 단계에서 명시적으로 주입하거나, 누락 시 fail-fast 되는 스크립트/설정을 같이 넣는 게 필요합니다.

return true
}

// MARK: - APNs Token

func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
print("APNS Token: \"\(token)\"")
FCMService.shared.setAPNSToken(deviceToken)
}

func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("APNs 등록 실패: \(error.localizedDescription)")
}
}
25 changes: 25 additions & 0 deletions Projects/App/Sources/DoriApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import FeatureHistory
import FeatureCalendar
import PlatformKakaoAuth
import PlatformKeychain
import PlatformFCM

@main
struct DoriApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
let store: StoreOf<AppFeature>
#if DEBUG
private let debugLaunchRoute: DebugLaunchRoute?
Expand Down Expand Up @@ -75,13 +77,24 @@ struct DoriApp: App {
networkService: networkService,
tokenStore: tokenStore
)
$0.fcmPushTestAPIClient = .live(networkService: networkService)
$0.notificationSettingsAPIClient = .live(networkService: networkService)
}

storeBox.store = store
self.store = store

FontManager.registerAllFonts()
KakaoSDKHandler.initializeFromMainBundle()

FCMService.shared.tokenRefreshHandler = { token in
try? tokenStore.saveFCMToken(token)
let endpoint = RegisterFCMTokenEndpoint(token: token)
_ = try? await networkService.request(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FCM 토큰 등록 실패를 try?로 버리고 있어서 로그인 전 토큰이 먼저 발급되면 서버 등록 실패 후 재시도 경로가 없습니다. 실제로 권한 요청은 앱 시작 .task에서 실행되고, 인증 토큰 저장은 로그인 성공 후라 순서가 쉽게 어긋날 수 있습니다. 저장된 FCM 토큰을 로그인 성공/앱 인증 완료 시점에 다시 업로드하거나, 인증 실패 케이스를 재시도 큐로 남겨야 푸시 미수신 사용자가 생기지 않습니다.

endpoint,
responseType: SuccessResponse<EmptyResponse>.self
)
}
}

var body: some Scene {
Expand Down Expand Up @@ -109,16 +122,21 @@ struct DoriApp: App {
.onOpenURL { url in
_ = KakaoSDKHandler.handleOpenURL(url)
}
.task {
await FCMService.shared.requestAuthorization()
}
}
}

#if DEBUG
private struct DebugLaunchRoute {
private let environment: [String: String]
private let route: String
private let memo: String

init?(environment: [String: String]) {
guard let route = environment["DORI_DEBUG_ROUTE"] else { return nil }
self.environment = environment
self.route = route
self.memo = environment["DORI_DEBUG_MEMO"] ?? ""
}
Expand All @@ -132,6 +150,11 @@ private struct DebugLaunchRoute {
AddDoriView(
store: Store(initialState: configuredState) {
AddDoriFeature()
} withDependencies: {
$0.addDoriAPIClient = .testValue
$0.userNotificationSettingsClient.isNotificationEnabled = {
environment["DORI_DEBUG_NOTIFICATION_ENABLED"] == "true"
}
}
)
}
Expand All @@ -144,6 +167,8 @@ private struct DebugLaunchRoute {
private var configuredState: AddDoriFeature.State {
var state = AddDoriFeature.State()
state.currentPage = 2
state.searchQuery = environment["DORI_DEBUG_PARTNER_NAME"] ?? "김철수"
state.amountInput.text = environment["DORI_DEBUG_AMOUNT"] ?? "100000"
state.memo = memo
return state
}
Expand Down
30 changes: 30 additions & 0 deletions Projects/App/UITests/Core/Base/TestUIBase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// TestUIBase.swift
// DoriAppUITests
//
// Created by Codex on 4/27/26.
//

import XCTest

@MainActor
class TestUIBase: XCTestCase {
let app = XCUIApplication()

override func setUpWithError() throws {
continueAfterFailure = false
}

override func tearDownWithError() throws {
app.terminate()
}

func waitForExistence(
_ element: XCUIElement,
timeout: TimeInterval = 5,
file: StaticString = #filePath,
line: UInt = #line
) {
XCTAssertTrue(element.waitForExistence(timeout: timeout), file: file, line: line)
}
}
18 changes: 18 additions & 0 deletions Projects/App/UITests/Core/Extensions/XCTestExpectation+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// XCTestExpectation+.swift
// DoriAppUITests
//
// Created by Codex on 4/27/26.
//

import XCTest

extension XCTestCase {
func waitBriefly(seconds: TimeInterval = 0.3) {
let expectation = expectation(description: "brief wait")
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
expectation.fulfill()
}
wait(for: [expectation], timeout: seconds + 1)
}
}
13 changes: 13 additions & 0 deletions Projects/App/UITests/Core/Protocols/UITestProtocols.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// UITestProtocols.swift
// DoriAppUITests
//
// Created by Codex on 4/27/26.
//

import XCTest

@MainActor
protocol UITestPage {
var app: XCUIApplication { get }
}
27 changes: 27 additions & 0 deletions Projects/App/UITests/Identifiers/TestIdentifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// TestIdentifier.swift
// DoriAppUITests
//
// Created by Codex on 4/27/26.
//

enum TestIdentifier {
enum LaunchEnvironment {
static let debugRoute = "DORI_DEBUG_ROUTE"
static let notificationEnabled = "DORI_DEBUG_NOTIFICATION_ENABLED"
static let partnerName = "DORI_DEBUG_PARTNER_NAME"
static let amount = "DORI_DEBUG_AMOUNT"
}

enum DebugRoute {
static let addDoriPage3 = "addDoriPage3"
}

enum AddDori {
static let submitButton = "완료"
static let notificationAlertTitle = "알림을 켜주세요"
static let notificationAlertDescription = "도리 일정을 놓치지 않도록\n시스템 알림 설정을 켜주세요"
static let openSettingsButton = "설정하기"
static let laterButton = "나중에"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// TestUIScenarioAddDoriNotificationSettings.swift
// DoriAppUITests
//
// Created by Codex on 4/27/26.
//

import XCTest

final class TestUIScenarioAddDoriNotificationSettings: TestUIBase {
func testShowNotificationSettingsAlertWhenSystemNotificationIsOffAfterAddingDori() {
app.launchEnvironment[TestIdentifier.LaunchEnvironment.debugRoute] = TestIdentifier.DebugRoute.addDoriPage3
app.launchEnvironment[TestIdentifier.LaunchEnvironment.notificationEnabled] = "false"
app.launchEnvironment[TestIdentifier.LaunchEnvironment.partnerName] = "김철수"
app.launchEnvironment[TestIdentifier.LaunchEnvironment.amount] = "100000"
app.launch()

let addDoriView = UIBaseAddDoriView(app: app)

waitForExistence(addDoriView.submitButton)
addDoriView.submit()

waitForExistence(addDoriView.notificationAlertTitle)
waitForExistence(addDoriView.notificationAlertDescription)
waitForExistence(addDoriView.openSettingsButton)
waitForExistence(addDoriView.laterButton)

addDoriView.laterButton.tap()
waitBriefly()

XCTAssertFalse(addDoriView.notificationAlertTitle.exists)
}
}
41 changes: 41 additions & 0 deletions Projects/App/UITests/TestPage/UIBase/UIBaseAddDoriView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// UIBaseAddDoriView.swift
// DoriAppUITests
//
// Created by Codex on 4/27/26.
//

import XCTest

@MainActor
final class UIBaseAddDoriView: UITestPage {
let app: XCUIApplication

init(app: XCUIApplication) {
self.app = app
}

var submitButton: XCUIElement {
app.buttons[TestIdentifier.AddDori.submitButton]
}

var notificationAlertTitle: XCUIElement {
app.staticTexts[TestIdentifier.AddDori.notificationAlertTitle]
}

var notificationAlertDescription: XCUIElement {
app.staticTexts[TestIdentifier.AddDori.notificationAlertDescription]
}

var openSettingsButton: XCUIElement {
app.buttons[TestIdentifier.AddDori.openSettingsButton]
}

var laterButton: XCUIElement {
app.buttons[TestIdentifier.AddDori.laterButton]
}

func submit() {
submitButton.tap()
}
}
39 changes: 39 additions & 0 deletions Projects/Core/DoriCore/Sources/BuildEnvironment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// BuildEnvironment.swift
// Dori-iOS
//
// Created by 강동영 on 4/18/26.
//

import Foundation

public enum BuildEnvironment: String, Sendable {
case debug
case qa
case release

public static let current: BuildEnvironment = {
#if DEBUG
return .debug
#elseif QA
return .qa
#else
return .release
#endif
}()

public var isTestingEnabled: Bool {
switch self {
case .debug, .qa: return true
case .release: return false
}
}

public var displayName: String {
switch self {
case .debug: return "DEBUG"
case .qa: return "QA"
case .release: return "RELEASE"
}
}
}
Loading
Loading