refactor(ads): Yandex 광고 코드와 SDK 의존성을 제거한다

This commit is contained in:
Yu Sung
2026-10-13 16:01:00 +09:00
parent c3b0d6e3f4
commit 9478565a04
21 changed files with 27 additions and 651 deletions
-4
View File
@@ -15,7 +15,6 @@ import FirebaseCore
import FirebaseAnalytics
import FirebaseMessaging
import LineSDK
import YandexMobileAds
class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -33,9 +32,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
Messaging.messaging().delegate = self
setupAppsFlyer()
// YandexAds 초기화
YandexAds.initializeSDK(completionHandler: nil)
// For iOS 10 display notification (sent via APNS)
UNUserNotificationCenter.current().delegate = self
@@ -36,11 +36,6 @@ struct CharacterView: View {
}
}
YandexInlineBannerView(
placement: .chatCharacterList,
horizontalPadding: 24
)
.padding(.vertical, -24)
// 인기 캐릭터 섹션
if !viewModel.popularCharacters.isEmpty {
@@ -22,10 +22,6 @@ struct OriginalTabView: View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 12) {
YandexInlineBannerView(
placement: .chatOriginalTabTop,
horizontalPadding: horizontalPadding
)
LazyVGrid(
columns: Array(
@@ -277,21 +277,6 @@ final class ChatRoomViewModel: ObservableObject {
purchaseChatQuota(chargeType: .can, canOption: canOption)
}
func showRewardedAdForChatQuota() {
_Concurrency.Task {
let isShown = await YandexRewardedAdManager.shared.showAdIfAvailable(for: .chatRoomQuota) { [weak self] in
self?.purchaseChatQuota(chargeType: .ad, canOption: nil)
}
if !isShown {
await MainActor.run {
errorMessage = I18n.Chat.Room.rewardedAdLoadFailed
isShowPopup = true
}
}
}
}
private func purchaseChatQuota(
chargeType: ChatRoomQuotaChargeType,
canOption: ChatRoomQuotaCanOption?
@@ -467,15 +452,6 @@ final class ChatRoomViewModel: ObservableObject {
private func updateQuota(totalRemaining: Int) {
self.totalRemaining = totalRemaining
showQuotaNoticeView = totalRemaining <= 0
prepareRewardedAdIfNeeded(totalRemaining: totalRemaining)
}
private func prepareRewardedAdIfNeeded(totalRemaining: Int) {
guard totalRemaining <= 1 else { return }
_Concurrency.Task {
await YandexRewardedAdManager.shared.preloadAd(for: .chatRoomQuota)
}
}
private func getSavedBackgroundImageId() -> Int? {
@@ -21,7 +21,6 @@ struct ChatQuotaPurchaseRequest: Encodable {
enum ChatRoomQuotaChargeType: String, Encodable {
case can = "CAN"
case ad = "AD"
}
enum ChatRoomQuotaCanOption: String, Encodable {
@@ -15,10 +15,6 @@ struct TalkView: View {
BaseView(isLoading: $viewModel.isLoading) {
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 24) {
YandexInlineBannerView(
placement: .chatTalkTabTop,
horizontalPadding: 24
)
if viewModel.talkRooms.isEmpty {
Text(I18n.Chat.Talk.emptyMessage)
@@ -1,316 +0,0 @@
//
// YandexAdSupport.swift
// SodaLive
//
// Created by OpenCode on 2026/04/28.
//
import SwiftUI
import YandexMobileAds
enum YandexBannerPlacement {
case liveTab
case liveDetail
case creatorCommunityAll
case seriesMainHome
case seriesMainDayOfWeek
case seriesMainByGenre
case pushNotificationList
case notificationReceiveSettings
case chatCharacterList
case chatOriginalTabTop
case chatTalkTabTop
}
enum YandexRewardedPlacement {
case chatRoomQuota
}
enum YandexAdUnitIdProvider {
static func banner(for placement: YandexBannerPlacement) -> String {
switch placement {
case .liveTab:
YANDEX_LIVE_TAB_BANNER_AD_UNIT_ID
case .liveDetail:
YANDEX_LIVE_DETAIL_BANNER_AD_UNIT_ID
case .creatorCommunityAll:
YANDEX_CREATOR_COMMUNITY_ALL_BANNER_AD_UNIT_ID
case .seriesMainHome:
YANDEX_SERIES_MAIN_HOME_BANNER_AD_UNIT_ID
case .seriesMainDayOfWeek:
YANDEX_SERIES_MAIN_DAY_OF_WEEK_BANNER_AD_UNIT_ID
case .seriesMainByGenre:
YANDEX_SERIES_MAIN_BY_GENRE_BANNER_AD_UNIT_ID
case .pushNotificationList:
YANDEX_PUSH_NOTIFICATION_LIST_BANNER_AD_UNIT_ID
case .notificationReceiveSettings:
YANDEX_NOTIFICATION_RECEIVE_SETTINGS_BANNER_AD_UNIT_ID
case .chatCharacterList:
YANDEX_CHAT_CHARACTER_LIST_BANNER_AD_UNIT_ID
case .chatOriginalTabTop:
YANDEX_CHAT_ORIGINAL_TAB_TOP_BANNER_AD_UNIT_ID
case .chatTalkTabTop:
YANDEX_CHAT_TALK_TAB_TOP_BANNER_AD_UNIT_ID
}
}
static func rewarded(for placement: YandexRewardedPlacement) -> String {
switch placement {
case .chatRoomQuota:
YANDEX_CHAT_ROOM_QUOTA_REWARDED_AD_UNIT_ID
}
}
}
struct YandexInlineBannerView: View {
let placement: YandexBannerPlacement
var maxHeight: CGFloat = 90
var horizontalPadding: CGFloat = 13.3
@State private var bannerHeight: CGFloat = 0
@State private var isLoadFailed = true
var body: some View {
GeometryReader { proxy in
let width = max(proxy.size.width - (horizontalPadding * 2), 1)
let resolvedHeight = isLoadFailed ? 0 : (bannerHeight > 0 ? bannerHeight : maxHeight)
YandexInlineBannerContainer(
placement: placement,
width: width,
maxHeight: maxHeight,
bannerHeight: $bannerHeight,
isLoadFailed: $isLoadFailed
)
.frame(width: width, height: resolvedHeight)
.padding(.horizontal, horizontalPadding)
}
.frame(height: isLoadFailed ? 0 : (bannerHeight > 0 ? bannerHeight : maxHeight))
}
}
private struct YandexInlineBannerContainer: UIViewRepresentable {
let placement: YandexBannerPlacement
let width: CGFloat
let maxHeight: CGFloat
@Binding var bannerHeight: CGFloat
@Binding var isLoadFailed: Bool
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.backgroundColor = .clear
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
context.coordinator.parent = self
context.coordinator.configureIfNeeded(containerView: uiView)
}
final class Coordinator: NSObject, BannerAdViewDelegate {
var parent: YandexInlineBannerContainer
private weak var containerView: UIView?
private var bannerAdView: BannerAdView?
private var currentWidth: CGFloat = 0
init(parent: YandexInlineBannerContainer) {
self.parent = parent
}
func configureIfNeeded(containerView: UIView) {
self.containerView = containerView
let roundedWidth = parent.width.rounded(.down)
guard bannerAdView == nil || abs(currentWidth - roundedWidth) > 0.5 else {
return
}
currentWidth = roundedWidth
DispatchQueue.main.async {
self.parent.bannerHeight = 0
self.parent.isLoadFailed = false
}
bannerAdView?.removeFromSuperview()
let adSize = BannerAdSize.inline(width: roundedWidth, maxHeight: parent.maxHeight)
let bannerAdView = BannerAdView(adSize: adSize)
bannerAdView.translatesAutoresizingMaskIntoConstraints = false
bannerAdView.delegate = self
containerView.addSubview(bannerAdView)
NSLayoutConstraint.activate([
bannerAdView.topAnchor.constraint(equalTo: containerView.topAnchor),
bannerAdView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
bannerAdView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
bannerAdView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])
self.bannerAdView = bannerAdView
bannerAdView.loadAd(with: AdRequest(adUnitID: YandexAdUnitIdProvider.banner(for: parent.placement)))
}
func bannerAdViewDidLoad(_ adView: BannerAdView) {
adView.layoutIfNeeded()
let measuredHeight = adView.bounds.height > 0 ? adView.bounds.height : parent.maxHeight
DispatchQueue.main.async {
self.parent.bannerHeight = measuredHeight
self.parent.isLoadFailed = false
}
}
func bannerAdViewDidFailLoading(_ adView: BannerAdView, error: Error) {
DispatchQueue.main.async {
self.parent.bannerHeight = 0
self.parent.isLoadFailed = true
}
}
func bannerAdViewDidClick(_ adView: BannerAdView) {
}
func bannerAdView(_ adView: BannerAdView, didTrackImpression impressionData: ImpressionData?) {
}
}
}
@MainActor
final class YandexRewardedAdManager: NSObject {
static let shared = YandexRewardedAdManager()
private var rewardedAd: RewardedAd?
private var rewardedAdLoader: RewardedAdLoader?
private var currentPlacement: YandexRewardedPlacement?
private var pendingRewardAction: (@MainActor () -> Void)?
private var rewardedPlacement: YandexRewardedPlacement?
private var isLoading = false
func preloadAd(for placement: YandexRewardedPlacement) {
guard !isLoading else {
return
}
if currentPlacement == placement, rewardedAd != nil {
return
}
let loader = RewardedAdLoader()
rewardedAdLoader = loader
rewardedAd = nil
currentPlacement = placement
isLoading = true
Task {
do {
let loadedAd = try await loader.loadAd(with: AdRequest(adUnitID: YandexAdUnitIdProvider.rewarded(for: placement)))
guard currentPlacement == placement else {
isLoading = false
return
}
loadedAd.delegate = self
rewardedAd = loadedAd
} catch {
if currentPlacement == placement {
rewardedAd = nil
}
}
if currentPlacement == placement {
isLoading = false
}
}
}
func showAdIfAvailable(for placement: YandexRewardedPlacement, onReward: @escaping @MainActor () -> Void) -> Bool {
guard let presenter = presentingViewController(), let rewardedAd, currentPlacement == placement else {
preloadAd(for: placement)
return false
}
pendingRewardAction = onReward
rewardedPlacement = placement
rewardedAd.show(from: presenter)
return true
}
private func completeRewardIfNeeded() {
let action = pendingRewardAction
pendingRewardAction = nil
action?()
}
private func resetAndPreload() {
pendingRewardAction = nil
rewardedAd = nil
if let rewardedPlacement {
self.rewardedPlacement = nil
preloadAd(for: rewardedPlacement)
}
}
private func presentingViewController() -> UIViewController? {
guard
let rootViewController = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.flatMap({ $0.windows })
.first(where: { $0.isKeyWindow })?
.rootViewController
else {
return nil
}
var topViewController = rootViewController
while let presentedViewController = topViewController.presentedViewController {
topViewController = presentedViewController
}
return topViewController
}
}
extension YandexRewardedAdManager: RewardedAdDelegate {
func rewardedAd(_ rewardedAd: RewardedAd, didReward reward: Reward) {
DEBUG_LOG("리워드 광고 보상 받기 성공")
completeRewardIfNeeded()
}
func rewardedAd(_ rewardedAd: RewardedAd, didFailToShow error: Error) {
DEBUG_LOG("리워드 광고 에러")
resetAndPreload()
}
func rewardedAdDidShow(_ rewardedAd: RewardedAd) {
}
func rewardedAdDidDismiss(_ rewardedAd: RewardedAd) {
DEBUG_LOG("리워드 광고 닫기")
resetAndPreload()
}
func rewardedAdDidClick(_ rewardedAd: RewardedAd) {
}
func rewardedAd(_ rewardedAd: RewardedAd, didTrackImpression impressionData: ImpressionData?) {
}
}
@@ -29,9 +29,6 @@ struct SeriesMainByGenreView: View {
let width = (screenSize().width - (horizontalPadding * 2) - gridSpacing) / 2
VStack(spacing: 16) {
if !viewModel.genreList.isEmpty {
YandexInlineBannerView(placement: .seriesMainByGenre, horizontalPadding: 24)
}
LazyVGrid(
columns: Array(
@@ -62,7 +62,6 @@ struct SeriesMainDayOfWeekView: View {
let width = (screenSize().width - (horizontalPadding * 2) - gridSpacing) / 2
VStack(spacing: 16) {
YandexInlineBannerView(placement: .seriesMainDayOfWeek, horizontalPadding: 24)
LazyVGrid(
columns: Array(
@@ -55,7 +55,6 @@ struct SeriesMainHomeView: View {
}
}
YandexInlineBannerView(placement: .seriesMainHome, horizontalPadding: 24)
if !viewModel.recommendSeriesList.isEmpty {
VStack(alignment: .leading, spacing: 16) {
@@ -11,7 +11,6 @@ let BASE_URL = "https://test-api.sodalive.net"
let APPLY_SODALIVE_CREATOR = "https://forms.gle/mmhJKmijjVRnwtdZ7"
let AGORA_APP_ID = "b96574e191a9430fa54c605528aa3ef7"
let AGORA_APP_CERTIFICATE = "ae18ade3afcf4086bd4397726eb0654c"
let BOOTPAY_APP_ID = "6242a7772701800023f68b2f"
let BOOTPAY_APP_HECTO_ID = "667fca5d3bab7404f831c3e5"
@@ -32,18 +31,3 @@ let LINE_CHANNEL_ID = "2008995582"
let AGORA_CUSTOMER_ID = "de5dd9ea151f4a43ba1ad8411817b169"
let AGORA_CUSTOMER_SECRET = "3855da8bc5ae4743af8bf4f87408b515"
let YANDEX_LIVE_TAB_BANNER_AD_UNIT_ID = "R-M-19140297-3"
let YANDEX_LIVE_DETAIL_BANNER_AD_UNIT_ID = "R-M-19140297-4"
let YANDEX_CONTENT_DETAIL_BANNER_AD_UNIT_ID = "R-M-19140297-5"
let YANDEX_CONTENT_DETAIL_INTERSTITIAL_AD_UNIT_ID = "R-M-19140297-6"
let YANDEX_CREATOR_COMMUNITY_ALL_BANNER_AD_UNIT_ID = "R-M-19140297-7"
let YANDEX_SERIES_MAIN_HOME_BANNER_AD_UNIT_ID = "R-M-19140297-8"
let YANDEX_SERIES_MAIN_DAY_OF_WEEK_BANNER_AD_UNIT_ID = "R-M-19140297-9"
let YANDEX_SERIES_MAIN_BY_GENRE_BANNER_AD_UNIT_ID = "R-M-19140297-10"
let YANDEX_PUSH_NOTIFICATION_LIST_BANNER_AD_UNIT_ID = "R-M-19140297-11"
let YANDEX_NOTIFICATION_RECEIVE_SETTINGS_BANNER_AD_UNIT_ID = "R-M-19140297-12"
let YANDEX_CHAT_CHARACTER_LIST_BANNER_AD_UNIT_ID = "R-M-19140297-13"
let YANDEX_CHAT_ORIGINAL_TAB_TOP_BANNER_AD_UNIT_ID = "R-M-19140297-14"
let YANDEX_CHAT_TALK_TAB_TOP_BANNER_AD_UNIT_ID = "R-M-19140297-15"
let YANDEX_CHAT_ROOM_QUOTA_REWARDED_AD_UNIT_ID = "R-M-19140297-16"
@@ -47,7 +47,6 @@ struct CreatorCommunityAllView: View {
communityViewTypeTabView
YandexInlineBannerView(placement: .creatorCommunityAll)
if isGridMode {
gridContentView
-16
View File
@@ -326,14 +326,6 @@ enum I18n {
pick(ko: "입력 중", en: "Typing", ja: "入力中")
}
static func quotaAdAction(chatCount: Int) -> String {
pick(
ko: "광고 / \(chatCount)채팅",
en: "Ad / \(chatCount) chats",
ja: "広告 / \(chatCount)チャット"
)
}
static func quotaChatCount(_ chatCount: Int) -> String {
pick(
ko: "\(chatCount)채팅",
@@ -342,14 +334,6 @@ enum I18n {
)
}
static var rewardedAdLoadFailed: String {
pick(
ko: "광고를 불러오지 못했어요\n잠시 후 다시 시도해 주세요.",
en: "Could not load the ad.\nPlease try again later.",
ja: "広告を読み込めませんでした。\nしばらくしてからもう一度お試しください。"
)
}
static var backgroundSelectionTitle: String {
pick(ko: "배경 이미지 선택", en: "Select background image", ja: "背景画像を選択")
}
-2
View File
@@ -91,8 +91,6 @@ struct LiveView: View {
SectionLatestFinishedLiveView(items: viewModel.latestFinishedLiveItems)
}
YandexInlineBannerView(placement: .liveTab)
.padding(.vertical, -24)
if viewModel.replayLiveItems.count > 0 {
LiveReplayListView(contentList: viewModel.replayLiveItems)
@@ -129,9 +129,6 @@ struct LiveDetailView: View {
.frame(width: proxy.size.width - 26.7)
}
YandexInlineBannerView(placement: .liveDetail, horizontalPadding: 0)
.frame(width: proxy.size.width - 26.7)
.padding(.top, 13.3)
Rectangle()
.frame(height: 1)
@@ -17,7 +17,6 @@ struct PushNotificationListView: View {
selectedTheme: $viewModel.selectedCategory
)
YandexInlineBannerView(placement: .pushNotificationList)
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 0) {
@@ -158,8 +158,6 @@ struct NotificationReceiveSettingsView: View {
.padding(.top, 13.3)
.padding(.horizontal, 13.3)
YandexInlineBannerView(placement: .notificationReceiveSettings)
.padding(.top, 26.7)
Text(I18n.Settings.Notification.followingChannels)
.appFont(size: 16.7, weight: .bold)
-16
View File
@@ -11,7 +11,6 @@ let BASE_URL = "https://api.sodalive.net"
let APPLY_SODALIVE_CREATOR = "https://forms.gle/mmhJKmijjVRnwtdZ7"
let AGORA_APP_ID = "e34e40046e9847baba3adfe2b8ffb4f6"
let AGORA_APP_CERTIFICATE = "15cadeea4ba94ff7b091c9a10f4bf4a6"
let BOOTPAY_APP_ID = "64c35be1d25985001dc50c88"
let BOOTPAY_APP_HECTO_ID = "664c1707b18b225deca4b42a"
@@ -32,18 +31,3 @@ let LINE_CHANNEL_ID = "2008995539"
let AGORA_CUSTOMER_ID = "de5dd9ea151f4a43ba1ad8411817b169"
let AGORA_CUSTOMER_SECRET = "3855da8bc5ae4743af8bf4f87408b515"
let YANDEX_LIVE_TAB_BANNER_AD_UNIT_ID = "R-M-19157621-1"
let YANDEX_LIVE_DETAIL_BANNER_AD_UNIT_ID = "R-M-19157621-2"
let YANDEX_CONTENT_DETAIL_BANNER_AD_UNIT_ID = "R-M-19157621-3"
let YANDEX_CONTENT_DETAIL_INTERSTITIAL_AD_UNIT_ID = "R-M-19157621-4"
let YANDEX_CREATOR_COMMUNITY_ALL_BANNER_AD_UNIT_ID = "R-M-19157621-5"
let YANDEX_SERIES_MAIN_HOME_BANNER_AD_UNIT_ID = "R-M-19157621-6"
let YANDEX_SERIES_MAIN_DAY_OF_WEEK_BANNER_AD_UNIT_ID = "R-M-19157621-7"
let YANDEX_SERIES_MAIN_BY_GENRE_BANNER_AD_UNIT_ID = "R-M-19157621-8"
let YANDEX_PUSH_NOTIFICATION_LIST_BANNER_AD_UNIT_ID = "R-M-19157621-9"
let YANDEX_NOTIFICATION_RECEIVE_SETTINGS_BANNER_AD_UNIT_ID = "R-M-19157621-10"
let YANDEX_CHAT_CHARACTER_LIST_BANNER_AD_UNIT_ID = "R-M-19157621-11"
let YANDEX_CHAT_ORIGINAL_TAB_TOP_BANNER_AD_UNIT_ID = "R-M-19157621-12"
let YANDEX_CHAT_TALK_TAB_TOP_BANNER_AD_UNIT_ID = "R-M-19157621-13"
let YANDEX_CHAT_ROOM_QUOTA_REWARDED_AD_UNIT_ID = "R-M-19157621-14"