diff --git a/Makefile b/Makefile deleted file mode 100644 index 2a5eaab9..00000000 --- a/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -# iOS(Tuist) 프로젝트 — 훅/자동화가 기대하는 표준 타깃 제공. -# 전체 테스트는 시간이 오래 걸려 CI/수동으로 돌린다: `mise exec -- tuist test` - -.PHONY: test generate - -test: - @echo "✅ make test: 훅 검증 스킵 (전체 테스트는 'mise exec -- tuist test')" - -generate: - tuist generate --no-open diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/TargetDependency+Module/Modules.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/TargetDependency+Module/Modules.swift index 2460b94f..df0657ac 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/TargetDependency+Module/Modules.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/TargetDependency+Module/Modules.swift @@ -65,6 +65,7 @@ public enum DomainModule: String, CaseIterable { case auth = "AuthDomain" case battle = "BattleDomain" case comment = "CommentDomain" + case ad = "AdDomain" case home = "HomeDomain" case notification = "NotificationDomain" case perspective = "PerspectiveDomain" diff --git a/Projects/Feature/Chat/Sources/Coordinator/Reducer/ChatCoordinator.swift b/Projects/App/Sources/Navigation/Chat/ChatCoordinator.swift similarity index 91% rename from Projects/Feature/Chat/Sources/Coordinator/Reducer/ChatCoordinator.swift rename to Projects/App/Sources/Navigation/Chat/ChatCoordinator.swift index 31a3d7b2..63084eeb 100644 --- a/Projects/Feature/Chat/Sources/Coordinator/Reducer/ChatCoordinator.swift +++ b/Projects/App/Sources/Navigation/Chat/ChatCoordinator.swift @@ -1,6 +1,6 @@ // // ChatCoordinator.swift -// Chat +// App // import Foundation @@ -9,6 +9,7 @@ import PickeCoreLogger import ChatInterface import CommentDomainInterface import ComposableArchitecture +import FeatureAssembly import PickeDesignKit import PickeCoreUtility import TCAFlow @@ -99,7 +100,13 @@ extension ChatCoordinator { action: IndexedRouterActionOf ) -> Effect { switch action { - case .routeAction(_, action: .preVote(.delegate(.dismiss))): + case let .routeAction(id, action: .preVote(.delegate(.dismiss))): + // 루트(사전투표)의 뒤로가기는 Chat 플로우 전체를 닫고, + // 스택 위에 올라온 최종투표 화면이면 한 단계만 닫아 대화방으로 돌아간다. + guard id == 0 else { + state.routes.goBack() + return .none + } return .send(.delegate(.dismiss)) case let .routeAction(_, action: .preVote(.delegate(.voteSubmitted(battleId, voteMode, _, isMindChanged)))): @@ -115,7 +122,9 @@ extension ChatCoordinator { // 사전투표 루트에서 감지된 재진입이면 사전투표 화면을 남기지 않고 관점 화면으로 교체. // (스택 중간 — 최종투표 중복 500 — 이면 기존처럼 push) if state.routes.count <= 1 { - state.routes = [.root(.comment(.init(battleId: battleId)), embedInNavigationView: true)] + // 배열을 통째로 갈아끼우면 TCAFlow 의 인덱스 기반 스크린 상태 캐시가 이전 화면 상태를 + // 그대로 붙들어 화면이 바뀌지 않는다. 같은 인덱스의 screen 만 교체한다. + state.routes[screenAt: 0] = .comment(.init(battleId: battleId)) } else { state.routes.push(.comment(.init(battleId: battleId))) } diff --git a/Projects/Feature/Chat/Sources/Coordinator/View/ChatCoordinatorView.swift b/Projects/App/Sources/Navigation/Chat/ChatCoordinatorView.swift similarity index 97% rename from Projects/Feature/Chat/Sources/Coordinator/View/ChatCoordinatorView.swift rename to Projects/App/Sources/Navigation/Chat/ChatCoordinatorView.swift index 5b302ac0..996bb680 100644 --- a/Projects/Feature/Chat/Sources/Coordinator/View/ChatCoordinatorView.swift +++ b/Projects/App/Sources/Navigation/Chat/ChatCoordinatorView.swift @@ -1,6 +1,6 @@ // // ChatCoordinatorView.swift -// Chat +// App // import Foundation @@ -8,6 +8,7 @@ import Foundation import SwiftUI import ComposableArchitecture +import FeatureAssembly import TCAFlow public struct ChatCoordinatorView: View { diff --git a/Projects/Domain/AdDomain/Interface/Sources/Model/FeedAd.swift b/Projects/Domain/AdDomain/Interface/Sources/Model/FeedAd.swift new file mode 100644 index 00000000..0235942f --- /dev/null +++ b/Projects/Domain/AdDomain/Interface/Sources/Model/FeedAd.swift @@ -0,0 +1,32 @@ +public struct FeedAd: Equatable, Sendable, Identifiable { + public let code: String + public let network: String + public let title: String + public let subtitle: String + public let imageURL: String + public let ctaText: String + public let clickURL: String + public let label: String + + public var id: String { code } + + public init( + code: String, + network: String, + title: String, + subtitle: String, + imageURL: String, + ctaText: String, + clickURL: String, + label: String + ) { + self.code = code + self.network = network + self.title = title + self.subtitle = subtitle + self.imageURL = imageURL + self.ctaText = ctaText + self.clickURL = clickURL + self.label = label + } +} diff --git a/Projects/Domain/AdDomain/Interface/Sources/UseCase/FeedAdInterface.swift b/Projects/Domain/AdDomain/Interface/Sources/UseCase/FeedAdInterface.swift new file mode 100644 index 00000000..3b22a357 --- /dev/null +++ b/Projects/Domain/AdDomain/Interface/Sources/UseCase/FeedAdInterface.swift @@ -0,0 +1,31 @@ +import ComposableArchitecture + +public protocol FeedAdInterface: Sendable { + func fetchAds() async throws -> [FeedAd] + func recordImpressions(codes: [String]) async throws +} + +public enum FeedAdUseCaseDependency: TestDependencyKey { + public static var testValue: FeedAdInterface { MockFeedAdClient() } +} + +public enum FeedAdRepositoryDependency: TestDependencyKey { + public static var testValue: FeedAdInterface { MockFeedAdClient() } +} + +public extension DependencyValues { + var feedAdRepository: FeedAdInterface { + get { self[FeedAdRepositoryDependency.self] } + set { self[FeedAdRepositoryDependency.self] = newValue } + } + + var feedAdUseCase: FeedAdInterface { + get { self[FeedAdUseCaseDependency.self] } + set { self[FeedAdUseCaseDependency.self] = newValue } + } +} + +private struct MockFeedAdClient: FeedAdInterface { + func fetchAds() async throws -> [FeedAd] { [] } + func recordImpressions(codes: [String]) async throws {} +} diff --git a/Projects/Domain/AdDomain/Project.swift b/Projects/Domain/AdDomain/Project.swift new file mode 100644 index 00000000..59b23a46 --- /dev/null +++ b/Projects/Domain/AdDomain/Project.swift @@ -0,0 +1,23 @@ +import Foundation + +import DependencyPackagePlugin +import DependencyPlugin +import ProjectTemplatePlugin + +import ProjectDescription + +let project = Project.makeModule( + name: "AdDomain", + bundleId: .appBundleID(name: ".AdDomain"), + product: .framework, + settings: .settings(), + dependencies: [ + .serviceAssembly, + ], + hasTests: true, + hasInterface: true, + interfaceDependencies: [ + .SPM.composableArchitecture, + ], + hasTesting: false +) diff --git a/Projects/Domain/AdDomain/Sources/AdLiveDependencies.swift b/Projects/Domain/AdDomain/Sources/AdLiveDependencies.swift new file mode 100644 index 00000000..a310ef82 --- /dev/null +++ b/Projects/Domain/AdDomain/Sources/AdLiveDependencies.swift @@ -0,0 +1,10 @@ +import AdDomainInterface +import ComposableArchitecture + +extension FeedAdUseCaseDependency: DependencyKey { + public static var liveValue: FeedAdInterface { FeedAdUseCaseImpl() } +} + +extension FeedAdRepositoryDependency: DependencyKey { + public static var liveValue: FeedAdInterface { FeedAdRepositoryImpl() } +} diff --git a/Projects/Domain/AdDomain/Sources/Model/FeedAdDTO.swift b/Projects/Domain/AdDomain/Sources/Model/FeedAdDTO.swift new file mode 100644 index 00000000..6f28a2ce --- /dev/null +++ b/Projects/Domain/AdDomain/Sources/Model/FeedAdDTO.swift @@ -0,0 +1,25 @@ +import AdDomainInterface + +struct FeedAdDTO: Decodable, Sendable { + let code: String + let network: String + let title: String + let subtitle: String + let imageUrl: String + let ctaText: String + let clickUrl: String + let label: String + + func toDomain() -> FeedAd { + FeedAd( + code: code, + network: network, + title: title, + subtitle: subtitle, + imageURL: imageUrl, + ctaText: ctaText, + clickURL: clickUrl, + label: label + ) + } +} diff --git a/Projects/Domain/AdDomain/Sources/Repository/FeedAdRepositoryImpl.swift b/Projects/Domain/AdDomain/Sources/Repository/FeedAdRepositoryImpl.swift new file mode 100644 index 00000000..ddf6423d --- /dev/null +++ b/Projects/Domain/AdDomain/Sources/Repository/FeedAdRepositoryImpl.swift @@ -0,0 +1,25 @@ +import APIEndpoint +import AdDomainInterface +import ComposableArchitecture +import PickeNetwork + +public struct FeedAdRepositoryImpl: FeedAdInterface { + @Dependency(\.networkClient) private var client + + public init() {} + + public func fetchAds() async throws -> [FeedAd] { + let data = try await client.send( + AdsService.list(query: AdsQueryRequest()), + as: [FeedAdDTO].self + ) + return data.map { $0.toDomain() } + } + + public func recordImpressions(codes: [String]) async throws { + _ = try await client.send( + AdsService.impressions(body: AdsImpressionsRequest(codes: codes)), + as: PickeEmptyResponse.self + ) + } +} diff --git a/Projects/Domain/AdDomain/Sources/UseCase/FeedAdUseCaseImpl.swift b/Projects/Domain/AdDomain/Sources/UseCase/FeedAdUseCaseImpl.swift new file mode 100644 index 00000000..f1ca953e --- /dev/null +++ b/Projects/Domain/AdDomain/Sources/UseCase/FeedAdUseCaseImpl.swift @@ -0,0 +1,16 @@ +import AdDomainInterface +import ComposableArchitecture + +public struct FeedAdUseCaseImpl: FeedAdInterface { + @Dependency(\.feedAdRepository) private var feedAdRepository + + public init() {} + + public func fetchAds() async throws -> [FeedAd] { + return try await feedAdRepository.fetchAds() + } + + public func recordImpressions(codes: [String]) async throws { + try await feedAdRepository.recordImpressions(codes: codes) + } +} diff --git a/Projects/Domain/AdDomain/Tests/Sources/AdDomainTests.swift b/Projects/Domain/AdDomain/Tests/Sources/AdDomainTests.swift new file mode 100644 index 00000000..809fdc6a --- /dev/null +++ b/Projects/Domain/AdDomain/Tests/Sources/AdDomainTests.swift @@ -0,0 +1,57 @@ +// +// AdTests.swift +// AdTests +// + +import Foundation +@testable import PickeNetwork +@testable import AdDomain +import AdDomainInterface +import APIEndpoint +import Testing + +struct AdDomainTests { + @Test func 광고_DTO를_도메인으로_매핑한다() { + let dto = FeedAdDTO( + code: "a1", + network: "ADPICK", + title: "상품", + subtitle: "상점 · 2.8%", + imageUrl: "https://example.com/image.jpg", + ctaText: "구매하러 가기", + clickUrl: "https://ad.picke.store/c/a1", + label: "광고" + ) + + #expect(dto.toDomain() == FeedAd( + code: "a1", + network: "ADPICK", + title: "상품", + subtitle: "상점 · 2.8%", + imageURL: "https://example.com/image.jpg", + ctaText: "구매하러 가기", + clickURL: "https://ad.picke.store/c/a1", + label: "광고" + )) + } + + @Test func 광고_조회_요청을_매핑한다() throws { + let request = try AdsService.list(query: AdsQueryRequest()).asURLRequest() + #expect(request.url?.path == "/api/v1/ads") + #expect(request.httpMethod == "GET") + #expect(request.url?.query?.contains("slot=HOME_FEED") == true) + #expect(request.url?.query?.contains("os=IOS") == true) + #expect(request.url?.query?.contains("size=20") == true) + } + + @Test func 광고_노출은_codes_JSON을_POST한다() throws { + let request = try AdsService.impressions( + body: AdsImpressionsRequest(codes: ["first", "second"]) + ).asURLRequest() + #expect(request.url?.path == "/api/v1/ads/impressions") + #expect(request.httpMethod == "POST") + let data = try #require(request.httpBody) + let body = try JSONDecoder().decode([String: [String]].self, from: data) + #expect(body == ["codes": ["first", "second"]]) + } +} diff --git a/Projects/Domain/BattleDomain/Sources/Model/DTO/BattleScenarioDataDTO.swift b/Projects/Domain/BattleDomain/Sources/Model/DTO/BattleScenarioDataDTO.swift index 9ac93c77..0e4bdb78 100644 --- a/Projects/Domain/BattleDomain/Sources/Model/DTO/BattleScenarioDataDTO.swift +++ b/Projects/Domain/BattleDomain/Sources/Model/DTO/BattleScenarioDataDTO.swift @@ -12,7 +12,8 @@ public struct BattleScenarioDataDTO: Decodable { public let title: String public let philosophers: [ScenarioPhilosopherDTO] public let isInteractive: Bool - public let startNodeId: Int + /// 서버가 비대화형 시나리오에서 null 을 내려주므로 옵셔널. 매핑 시 첫 노드로 폴백한다. + public let startNodeId: Int? public let recommendedPathKey: String public let audios: [String: String] public let nodes: [ScenarioNodeDTO] diff --git a/Projects/Domain/BattleDomain/Sources/Model/Mapper/BattleScenarioDataDTO+.swift b/Projects/Domain/BattleDomain/Sources/Model/Mapper/BattleScenarioDataDTO+.swift index 1dc41416..2e7febb1 100644 --- a/Projects/Domain/BattleDomain/Sources/Model/Mapper/BattleScenarioDataDTO+.swift +++ b/Projects/Domain/BattleDomain/Sources/Model/Mapper/BattleScenarioDataDTO+.swift @@ -15,7 +15,7 @@ public extension BattleScenarioDataDTO { dto.toDomain(fallbackLabel: Self.fallbackLabel(for: idx)) }, isInteractive: isInteractive, - startNodeId: startNodeId, + startNodeId: startNodeId ?? nodes.first?.nodeId ?? 0, recommendedPathKey: RecommendedPathKey(rawValue: recommendedPathKey), audios: audios, nodes: nodes.map { $0.toDomain() } diff --git a/Projects/Domain/BattleDomain/Tests/Sources/BattleRepositoryTests.swift b/Projects/Domain/BattleDomain/Tests/Sources/BattleRepositoryTests.swift index 5e2e6cd8..15027fae 100644 --- a/Projects/Domain/BattleDomain/Tests/Sources/BattleRepositoryTests.swift +++ b/Projects/Domain/BattleDomain/Tests/Sources/BattleRepositoryTests.swift @@ -388,6 +388,21 @@ struct BattleRepositoryTests { #expect(perspective == nil) } + @Test func fetchMyPerspective_는_data_가_빈_배열이면_nil_을_반환한다() async throws { + let json = """ + { "statusCode": 200, "data": [] } + """ + let repo = withDependencies { + $0.networkClient = StubNetworkClient(stubData: Data(json.utf8)) + } operation: { + BattleRepositoryImpl() + } + + let perspective = try await repo.fetchMyPerspective(battleId: 1) + + #expect(perspective == nil) + } + @Test func fetchMyPerspective_는_provider_가_에러를_던지면_nil_을_반환한다() async throws { let repo = withDependencies { $0.networkClient = ThrowingStubNetworkClient() @@ -474,6 +489,51 @@ struct BattleRepositoryTests { #expect(scenario.philosophers[1].label == "B") } + @Test func fetchScenario_는_startNodeId_가_null_이면_첫_노드를_시작_노드로_쓴다() async throws { + let json = """ + { + "statusCode": 200, + "data": { + "battleId": 5, + "title": "시나리오 제목", + "philosophers": [{"label": "A", "name": "소크라테스", "stance": "PRO", "imageUrl": "https://img.picke.app/s.png"}], + "isInteractive": false, + "startNodeId": null, + "recommendedPathKey": "COMMON", + "audios": {}, + "nodes": [ + { + "nodeId": 11, + "nodeName": "START", + "audioDuration": 85, + "autoNextNodeId": null, + "scripts": [], + "interactiveOptions": [] + }, + { + "nodeId": 12, + "nodeName": "CLOSING", + "audioDuration": 11, + "autoNextNodeId": null, + "scripts": [], + "interactiveOptions": [] + } + ] + }, + "error": null + } + """ + let repo = withDependencies { + $0.networkClient = StubNetworkClient(stubData: Data(json.utf8)) + } operation: { + BattleRepositoryImpl() + } + + let scenario = try await repo.fetchScenario(battleId: 5) + + #expect(scenario.startNodeId == 11) + } + // MARK: - fetchRecommendedBattles @Test func fetchRecommendedBattles_는_봉투_data_를_도메인으로_매핑한다() async throws { diff --git a/Projects/Domain/DomainAssembly/Project.swift b/Projects/Domain/DomainAssembly/Project.swift index d525ca0b..4a3ba505 100644 --- a/Projects/Domain/DomainAssembly/Project.swift +++ b/Projects/Domain/DomainAssembly/Project.swift @@ -16,6 +16,7 @@ let project = Project.makeModule( .domain(.attendance, .implementation), .domain(.auth, .implementation), .domain(.battle, .implementation), + .domain(.ad, .implementation), .domain(.search, .implementation), .domain(.comment, .implementation), .domain(.home, .implementation), @@ -24,4 +25,4 @@ let project = Project.makeModule( .domain(.profile, .implementation), ], hasTests: true -) \ No newline at end of file +) diff --git a/Projects/Domain/DomainAssembly/Sources/Exported/DomainAssemblyExported.swift b/Projects/Domain/DomainAssembly/Sources/Exported/DomainAssemblyExported.swift index 0aba6fc3..04377248 100644 --- a/Projects/Domain/DomainAssembly/Sources/Exported/DomainAssemblyExported.swift +++ b/Projects/Domain/DomainAssembly/Sources/Exported/DomainAssemblyExported.swift @@ -13,6 +13,8 @@ @_exported import AuthDomainInterface @_exported import BattleDomain @_exported import BattleDomainInterface +@_exported import AdDomain +@_exported import AdDomainInterface @_exported import CommentDomain @_exported import CommentDomainInterface @_exported import HomeDomain diff --git a/Projects/Feature/Ad/Project.swift b/Projects/Feature/Ad/Project.swift index 6bb3ffc9..e8ed24c2 100644 --- a/Projects/Feature/Ad/Project.swift +++ b/Projects/Feature/Ad/Project.swift @@ -12,14 +12,19 @@ let project = Project.makeModule( settings: .settings(), dependencies: [ .core(.logger), + .domain(.ad, .interface), + .ui(.designKit), + .ui(.sharedUI), + .feature(.featureSharedUI, .implementation), + .SPM.composableArchitecture, .SPM.adFit, .SPM.googleMobileAds, .service(.analytics, .interface), ], - hasTests: true, + hasTests: false, hasInterface: true, interfaceDependencies: [ .SPM.composableArchitecture, ], hasTesting: false -) \ No newline at end of file +) diff --git a/Projects/Feature/Ad/Sources/FeedAdRow.swift b/Projects/Feature/Ad/Sources/FeedAdRow.swift new file mode 100644 index 00000000..ebeccba7 --- /dev/null +++ b/Projects/Feature/Ad/Sources/FeedAdRow.swift @@ -0,0 +1,103 @@ +import SwiftUI +import AdDomainInterface +import PickeSharedUI +import PickeDesignKit + +public struct FeedAdRow: View { + private let ad: FeedAd + private let viewport: CGRect + private let onVisible: () -> Void + private let onAdClick: () -> Void + @State private var isVisible = false + @Environment(\.openURL) private var openURL + @Environment(\.scenePhase) private var scenePhase + + public init( + ad: FeedAd, + viewport: CGRect, + onVisible: @escaping () -> Void = {}, + onAdClick: @escaping () -> Void = {} + ) { + self.ad = ad + self.viewport = viewport + self.onVisible = onVisible + self.onAdClick = onAdClick + } + + public var body: some View { + Button { + if let url = URL(string: ad.clickURL), + ["https", "http"].contains(url.scheme?.lowercased() ?? "") { + onAdClick() + openURL(url) + } + } label: { + content() + } + .buttonStyle(.plain) + .background { + GeometryReader { geometry in + Color.clear + .preference( + key: FeedAdVisibilityPreference.self, + value: geometry.frame(in: .global).intersection(viewport).height > 0 + && geometry.frame(in: .global).intersects(viewport) + ) + } + } + .onPreferenceChange(FeedAdVisibilityPreference.self) { visible in + isVisible = visible + if visible, scenePhase == .active { onVisible() } + } + .onChange(of: scenePhase) { _, phase in + if phase == .active, isVisible { onVisible() } + } + } +} + +extension FeedAdRow { + @ViewBuilder + private func content() -> some View { + HStack(spacing: 12) { + thumbnail() + details() + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(.beige100) + } + + @ViewBuilder + private func thumbnail() -> some View { + Group { + if let url = URL(string: ad.imageURL) { + PickeRemoteImage(url: url) { Color.beige600 } + } else { + Color.beige600 + } + } + .frame(width: 80, height: 100) + .clipped() + .clipShape(RoundedRectangle(cornerRadius: .radiusDefault)) + } + + @ViewBuilder + private func details() -> some View { + VStack(alignment: .leading, spacing: 6) { + Text(ad.label) + .pretendardFont(.labelSmall) + .foregroundStyle(.neutral400) + Text(ad.title) + .pretendardFont(.headingSmall) + .foregroundStyle(.neutral500) + .lineLimit(2) + Text(ad.subtitle) + .pretendardFont(.regular13) + .foregroundStyle(.neutral400) + .lineLimit(1) + Text(ad.ctaText) + .pretendardFont(.labelSmall) + .foregroundStyle(.primary500) + } + } +} diff --git a/Projects/Feature/Ad/Sources/FeedAdVisibilityPreference.swift b/Projects/Feature/Ad/Sources/FeedAdVisibilityPreference.swift new file mode 100644 index 00000000..383cdce9 --- /dev/null +++ b/Projects/Feature/Ad/Sources/FeedAdVisibilityPreference.swift @@ -0,0 +1,12 @@ +import SwiftUI + +struct FeedAdVisibilityPreference: PreferenceKey { + static let defaultValue = false + + static func reduce( + value: inout Bool, + nextValue: () -> Bool + ) { + value = value || nextValue() + } +} diff --git a/Projects/Feature/Ad/Sources/MixedNativeAdView.swift b/Projects/Feature/Ad/Sources/MixedNativeAdView.swift new file mode 100644 index 00000000..0b59e98f --- /dev/null +++ b/Projects/Feature/Ad/Sources/MixedNativeAdView.swift @@ -0,0 +1,115 @@ +import ComposableArchitecture +import FeatureSharedUI +import AdDomainInterface +import SwiftUI +import PickeCoreLogger + +/// A single ad placement that alternates between a server ad and Kakao AdFit. +/// The selected source remains stable for the lifetime of the visible placement. +public struct MixedNativeAdView: View { + private let unit: AdFitNativeAdUnit + private let insets: EdgeInsets + private let onAdClick: () -> Void + private let onServerAdClick: (String) -> Void + private let placementKey: String + private let viewport: CGRect + + @Dependency(\.feedAdUseCase) private var feedAdUseCase + @Environment(\.scenePhase) private var scenePhase + @State private var serverAd: FeedAd? + @State private var isLoading = true + @State private var useServer = true + @State private var hasAppeared = false + @State private var didRecordImpression = false + + public init( + unit: AdFitNativeAdUnit = .wide, + insets: EdgeInsets = EdgeInsets(), + placementKey: String = "default", + viewport: CGRect, + onAdClick: @escaping () -> Void = {}, + onServerAdClick: @escaping (String) -> Void = { _ in } + ) { + self.unit = unit + self.insets = insets + self.placementKey = placementKey + self.viewport = viewport + self.onAdClick = onAdClick + self.onServerAdClick = onServerAdClick + } + + public var body: some View { + VStack(spacing: 0) { + adContent() + } + .task { + beginAppearanceIfNeeded() + await loadServerAdIfNeeded() + } + .onDisappear { hasAppeared = false } + } +} + +private extension MixedNativeAdView { + @ViewBuilder + func adContent() -> some View { + if let serverAd { + FeedAdRow( + ad: serverAd, + viewport: viewport, + onVisible: { recordImpression(for: serverAd) }, + onAdClick: { onServerAdClick(serverAd.network) } + ) + .padding(insets) + } else if isLoading { + ProgressView() + .frame(maxWidth: .infinity) + .frame(height: 132) + .padding(insets) + } else { + AdFitNativeAdView( + unit: unit, + insets: insets, + onAdClick: onAdClick + ) + } + } + + func beginAppearanceIfNeeded() { + guard !hasAppeared else { return } + hasAppeared = true + didRecordImpression = false + serverAd = nil + isLoading = false + useServer = !UserDefaults.standard.bool(forKey: placementKey) + UserDefaults.standard.set(useServer, forKey: placementKey) + isLoading = useServer + } + + @MainActor + func loadServerAdIfNeeded() async { + guard hasAppeared, useServer else { return } + isLoading = true + do { + let ads = try await feedAdUseCase.fetchAds() + guard !Task.isCancelled else { return } + serverAd = ads.first + } catch { + guard !Task.isCancelled else { return } + PickeLogger.error("[FeedAd] 광고 조회 실패: \(error.localizedDescription)", category: .ui) + } + isLoading = false + } + + func recordImpression(for ad: FeedAd) { + guard scenePhase == .active, !didRecordImpression else { return } + didRecordImpression = true + Task { + do { + try await feedAdUseCase.recordImpressions(codes: [ad.code]) + } catch { + PickeLogger.error("[FeedAd] 광고 노출 전송 실패: \(error.localizedDescription)", category: .ui) + } + } + } +} diff --git a/Projects/Feature/Ad/Tests/Sources/AdTests.swift b/Projects/Feature/Ad/Tests/Sources/AdTests.swift deleted file mode 100644 index 677960b1..00000000 --- a/Projects/Feature/Ad/Tests/Sources/AdTests.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// AdTests.swift -// AdTests -// - -@testable import Ad -import Testing - -struct AdTests { - @Test - func adServiceExample() { - #expect(true) - } -} diff --git a/Projects/Feature/Chat/Project.swift b/Projects/Feature/Chat/Project.swift index c37b240a..a45fa76e 100644 --- a/Projects/Feature/Chat/Project.swift +++ b/Projects/Feature/Chat/Project.swift @@ -26,9 +26,9 @@ let project = Project.makeModule( .core(.network), .domain(.comment, .interface), - .feature(.featureSharedUI, .implementation), + .feature(.ad, .implementation), ], hasTests: true, hasInterface: true, hasTesting: false -) \ No newline at end of file +) diff --git a/Projects/Feature/Chat/Sources/Curation/Reducer/CurationFeature.swift b/Projects/Feature/Chat/Sources/Curation/Reducer/CurationFeature.swift index 7c7b895d..f9154bb6 100644 --- a/Projects/Feature/Chat/Sources/Curation/Reducer/CurationFeature.swift +++ b/Projects/Feature/Chat/Sources/Curation/Reducer/CurationFeature.swift @@ -45,6 +45,7 @@ public struct CurationFeature { case closeButtonTapped case battleTapped(battleId: Int) case adNativeClicked + case serverAdClicked(network: String) } public enum AsyncAction: Equatable { @@ -108,6 +109,14 @@ extension CurationFeature { analyticsUseCase.track(.uiAction(action: .curationBattle, screen: .curation)) return .send(.delegate(.openBattle(battleId: battleId))) + case let .serverAdClicked(network): + analyticsUseCase.track(.adClick(AdClickData( + placement: .curation, + format: .native, + unit: network + ))) + return .none + case .adNativeClicked: analyticsUseCase.track(.adClick(AdClickData(placement: .curation, format: .native, unit: "ADFIT_NATIVE_2_1"))) return .none diff --git a/Projects/Feature/Chat/Sources/Curation/View/CurationView.swift b/Projects/Feature/Chat/Sources/Curation/View/CurationView.swift index d0daaa94..c693aa10 100644 --- a/Projects/Feature/Chat/Sources/Curation/View/CurationView.swift +++ b/Projects/Feature/Chat/Sources/Curation/View/CurationView.swift @@ -10,7 +10,7 @@ import ComposableArchitecture import PickeDesignKit import PickeSharedUI import PickeCoreUtility -import FeatureSharedUI +import Ad @ViewAction(for: CurationFeature.self) public struct CurationView: View { @@ -23,29 +23,33 @@ public struct CurationView: View { public var body: some View { VStack(spacing: 0) { header() - ScrollView(showsIndicators: false) { - VStack(spacing: 16) { - // 큐레이션 리스트 최상단 네이티브 광고 — 로딩/빈 상태와 무관하게 항상 노출한다. - // 광고가 없으면 AdFitNativeAdView 가 스스로 자리를 접어 높이 0 이 된다. - AdFitNativeAdView( - unit: .wide, - insets: EdgeInsets(top: 0, leading: 0, bottom: 4, trailing: 0), - onAdClick: { send(.adNativeClicked) } - ) - - if store.viewState == .loading, store.battles.isEmpty { - CurationSkeletonView() - } else if store.battles.isEmpty { - emptyState() - } else { - ForEach(store.battles) { battle in - battleCard(battle) + GeometryReader { viewport in + ScrollView(showsIndicators: false) { + VStack(spacing: 16) { + // 큐레이션 리스트 최상단 광고 — 서버 광고와 Kakao 광고를 번갈아 노출한다. + MixedNativeAdView( + unit: .wide, + insets: EdgeInsets(top: 0, leading: 0, bottom: 4, trailing: 0), + placementKey: "mixedNativeAd.curation", + viewport: viewport.frame(in: .global), + onAdClick: { send(.adNativeClicked) }, + onServerAdClick: { send(.serverAdClicked(network: $0)) } + ) + + if store.viewState == .loading, store.battles.isEmpty { + CurationSkeletonView() + } else if store.battles.isEmpty { + emptyState() + } else { + ForEach(store.battles) { battle in + battleCard(battle) + } } } + .padding(.horizontal, 16) + .padding(.top, 16) + .padding(.bottom, 16) } - .padding(.horizontal, 16) - .padding(.top, 16) - .padding(.bottom, 16) } } .screenBackground() diff --git a/Projects/Feature/Chat/Sources/Vote/View/PreVoteView.swift b/Projects/Feature/Chat/Sources/Vote/View/PreVoteView.swift index e72faf5c..e6a3b2c6 100644 --- a/Projects/Feature/Chat/Sources/Vote/View/PreVoteView.swift +++ b/Projects/Feature/Chat/Sources/Vote/View/PreVoteView.swift @@ -97,19 +97,22 @@ public struct PreVoteView: View { backgroundImage(battle) .frame(width: proxy.size.width) - // 큰 화면에서는 옵션 카드를 CTA 바로 위에 유지하고, 긴 제목/작은 화면에서는 - // 콘텐츠 영역만 스크롤해 제목 전체를 읽을 수 있게 한다. - let contentHeight = max(0, proxy.size.height - topInset - PreVoteLayout.ctaReservedHeight) - VStack(spacing: 0) { - Color.clear - .frame(height: topInset) - - ScrollView { - contentArea(battle, minHeight: contentHeight) + // 히어로 오버랩 여백을 스크롤 콘텐츠 안에 둔다. 공간이 남는 화면(16 Pro 등)에서는 + // minHeight 로 뷰포트를 채워 옵션 카드가 CTA 바로 위에 붙고, 부족한 화면에서는 + // 여백까지 함께 스크롤돼 카드가 잘린 채 고정되지 않는다. + ScrollView { + VStack(spacing: 0) { + Color.clear + .frame(height: topInset) + + contentArea(battle) } - .scrollIndicators(.hidden) - .frame(height: contentHeight, alignment: .top) + .frame( + minHeight: max(0, proxy.size.height - PreVoteLayout.ctaReservedHeight), + alignment: .top + ) } + .scrollIndicators(.hidden) .frame(width: proxy.size.width, height: proxy.size.height, alignment: .top) .safeAreaInset(edge: .bottom, spacing: 0) { primaryButton() @@ -201,7 +204,7 @@ extension PreVoteView { extension PreVoteView { @ViewBuilder - private func contentArea(_ battle: PreVoteBattle, minHeight: CGFloat) -> some View { + private func contentArea(_ battle: PreVoteBattle) -> some View { VStack(spacing: 0) { contentSection(battle) // 유연 간격: 콘텐츠는 위(상단 spacer)에 고정, 옵션은 아래로 당겨 CTA 위 40 유지. @@ -212,9 +215,6 @@ extension PreVoteView { .padding(.top, PreVoteLayout.contentTopPadding) .padding(.bottom, PreVoteLayout.contentBottomSpacing) .frame(maxWidth: .infinity) - // 큰 화면: 뷰포트를 채워 옵션을 하단 고정(기존 디자인). 작은 화면: 콘텐츠가 넘치면 - // 이 프레임이 그대로 늘어나 스크롤 영역이 되고, 옵션이 CTA 밑으로 잘리지 않는다. - .frame(minHeight: minHeight, alignment: .top) .background( LinearGradient( stops: [ diff --git a/Projects/Feature/Hifi/Project.swift b/Projects/Feature/Hifi/Project.swift index 1abe1212..5b2b88a5 100644 --- a/Projects/Feature/Hifi/Project.swift +++ b/Projects/Feature/Hifi/Project.swift @@ -20,6 +20,8 @@ let project = Project.makeModule( .service(.analytics, .interface), // 탐색 리스트 인라인 배너 광고 — 광고를 노출하는 화면만 명시적으로 의존한다. .feature(.featureSharedUI, .implementation), + .feature(.ad, .implementation), + .domain(.ad, .interface), .domain(.home, .interface), .domain(.search, .interface), .domain(.notification, .interface), @@ -29,4 +31,4 @@ let project = Project.makeModule( hasTests: true, hasInterface: true, hasTesting: false -) \ No newline at end of file +) diff --git a/Projects/Feature/Hifi/Sources/Reducer/HifiFeature.swift b/Projects/Feature/Hifi/Sources/Reducer/HifiFeature.swift index 0dff23e8..dfafd212 100644 --- a/Projects/Feature/Hifi/Sources/Reducer/HifiFeature.swift +++ b/Projects/Feature/Hifi/Sources/Reducer/HifiFeature.swift @@ -4,6 +4,7 @@ // import Foundation +import AdDomainInterface import PickeCoreLogger import SearchDomainInterface @@ -23,7 +24,16 @@ public struct HifiFeature { public var categories: [ExploreCategory] = ExploreCategory.allCases public var selectedCategory: ExploreCategory = .all public var selectedSort: ExploreSort = .popular - public var items: [ExploreItem] = [] + public var exploreItems: [ExploreItem] = [] + public var ads: [FeedAd] = [] + public var reportedAdCodesByItemID: [Int: Set] = [:] + + public func ad(after index: Int) -> FeedAd? { + guard index >= 0, (index + 1).isMultiple(of: 3) else { return nil } + let adIndex = (index + 1) / 3 - 2 + guard adIndex >= 0, !ads.isEmpty else { return nil } + return ads[adIndex % ads.count] + } /// 화면이 스켈레톤을 보일지 콘텐츠를 보일지 가르는 상태. public enum ViewState: Equatable { case loading @@ -58,24 +68,31 @@ public struct HifiFeature { case notificationTapped /// 탐색 화면 배너 광고 클릭 case adBannerClicked + case serverAdClicked(network: String) + case adVisible(code: String, itemID: Int) } public enum AsyncAction: Equatable { case searchRequested(reset: Bool) /// 벨 배지용 미읽음 여부 서버 동기화 (GET /api/v1/notifications/unread). case syncUnreadBadge + case fetchAds } public enum InnerAction: Equatable { case searchResponse(Result, reset: Bool) case unreadBadgeResponse(Bool) + case adsResponse(Result<[FeedAd], BattleError>) + case impressionResponse(code: String, itemID: Int, result: Result) } nonisolated enum CancelID: Hashable { case search + case fetchAds case syncUnreadBadge } + @Dependency(\.feedAdUseCase) private var feedAdUseCase @Dependency(\.searchUseCase) private var searchUseCase @Dependency(\.notificationUseCase) private var notificationUseCase @Dependency(\.analyticsUseCase) private var analyticsUseCase @@ -110,7 +127,8 @@ extension HifiFeature { // 벨 배지는 진입/재진입마다 서버(/unread)로 갱신 — 저장값 없이 서버 진실값만 사용. return .merge( .send(.async(.searchRequested(reset: true))), - .send(.async(.syncUnreadBadge)) + .send(.async(.syncUnreadBadge)), + .send(.async(.fetchAds)) ) case let .categoryTapped(category): @@ -138,9 +156,29 @@ extension HifiFeature { guard state.hasNext, state.viewState != .loading else { return .none } return .send(.async(.searchRequested(reset: false))) + case let .serverAdClicked(network): + analyticsUseCase.track(.adClick(AdClickData( + placement: .explore, + format: .native, + unit: network + ))) + return .none + case .adBannerClicked: analyticsUseCase.track(.adClick(AdClickData(placement: .explore, format: .banner, unit: "ADFIT_BANNER_320X100"))) return .none + + case let .adVisible(code, itemID): + guard state.ads.contains(where: { $0.code == code }), + state.reportedAdCodesByItemID[itemID, default: []].insert(code).inserted else { return .none } + return .run { [useCase = feedAdUseCase] send in + let result = await Result { + try await useCase.recordImpressions(codes: [code]) + return true + } + .mapError(BattleError.from) + await send(.inner(.impressionResponse(code: code, itemID: itemID, result: result))) + } } } @@ -160,7 +198,10 @@ extension HifiFeature { switch action { case let .searchRequested(reset): state.viewState = .loading - if reset { state.items = [] } + if reset { + state.exploreItems = [] + state.reportedAdCodesByItemID = [:] + } let category = state.selectedCategory.queryValue let sort = state.selectedSort.queryValue let offset = reset ? 0 : (state.nextOffset ?? 0) @@ -173,6 +214,14 @@ extension HifiFeature { } .cancellable(id: CancelID.search, cancelInFlight: true) + case .fetchAds: + return .run { [useCase = feedAdUseCase] send in + let result = await Result { try await useCase.fetchAds() } + .mapError(BattleError.from) + await send(.inner(.adsResponse(result))) + } + .cancellable(id: CancelID.fetchAds, cancelInFlight: true) + case .syncUnreadBadge: return .run { [useCase = notificationUseCase] send in guard let hasUnread = try? await useCase.hasUnreadNotifications() else { return } @@ -191,12 +240,25 @@ extension HifiFeature { state.viewState = .loaded switch result { case let .success(page): - state.items = reset ? page.items : state.items + page.items + state.exploreItems = reset ? page.items : state.exploreItems + page.items state.nextOffset = page.nextOffset state.hasNext = page.hasNext case let .failure(error): PickeLogger.error("[HifiFeature] searchBattles failed: \(error.localizedDescription)", category: .ui) - if reset { state.items = [] } + if reset { state.exploreItems = [] } + } + return .none + + case let .adsResponse(result): + switch result { + case let .success(ads): state.ads = ads + case .failure: state.ads = [] + } + return .none + + case let .impressionResponse(code, itemID, result): + if case .failure = result { + state.reportedAdCodesByItemID[itemID]?.remove(code) } return .none diff --git a/Projects/Feature/Hifi/Sources/View/HifiView.swift b/Projects/Feature/Hifi/Sources/View/HifiView.swift index 90166406..23533738 100644 --- a/Projects/Feature/Hifi/Sources/View/HifiView.swift +++ b/Projects/Feature/Hifi/Sources/View/HifiView.swift @@ -4,8 +4,9 @@ // import SwiftUI - +import Ad import FeatureSharedUI + import ComposableArchitecture import HomeDomainInterface import PickeSharedUI @@ -21,10 +22,6 @@ public struct HifiView: View { } public var body: some View { - // 상단 바는 스크롤 영향 없는 sticky 헤더 — Home 과 동일하게 VStack 최상단에 둔다. - // (기존 `.safeAreaInset(edge: .top)` + 바 배경 `.ignoresSafeArea(edges: .top)` 조합은 - // iPhone 13 mini / iOS 18.6 에서 상단 안전영역 인셋이 이중 계산돼 헤더가 아래로 - // 밀리는 기종-한정 오류를 유발했다.) VStack(spacing: 0) { fixedTopBar() contentArea() @@ -58,9 +55,9 @@ private extension HifiView { @ViewBuilder func contentArea() -> some View { Group { - if store.viewState == .loading, store.items.isEmpty { + if store.viewState == .loading, store.exploreItems.isEmpty { skeletonList() - } else if store.items.isEmpty { + } else if store.exploreItems.isEmpty { emptyState() } else { exploreList() @@ -107,43 +104,45 @@ private extension HifiView { @ViewBuilder func exploreList() -> some View { - ScrollView { - LazyVStack(spacing: 0) { - ForEach(Array(store.items.enumerated()), id: \.element.id) { index, item in - // 마지막 카드 아래엔 구분선을 그리지 않는다. - exploreRow(item, showsDivider: index != store.items.count - 1) - .onAppear { - // 무한 스크롤: 마지막 아이템 노출 시 다음 페이지 로드 - if item.id == store.items.last?.id { - send(.reachedBottom) + GeometryReader { viewport in + ScrollView { + LazyVStack(spacing: 0) { + ForEach(store.exploreItems) { item in + // 마지막 카드 아래엔 구분선을 그리지 않는다. + exploreRow(item, showsDivider: item.id != store.exploreItems.last?.id) + .onAppear { + // 무한 스크롤: 마지막 아이템 노출 시 다음 페이지 로드 + if item.id == store.exploreItems.last?.id { + send(.reachedBottom) + } } - } - // AdFit 광고 단위 코드는 한 화면에 한 번만 노출 가능해 3번째 카드 뒤에만 넣는다. - if index == 2, index != store.items.count - 1 { - adBannerRow() + if item.id == store.exploreItems.dropFirst(2).first?.id { + AdFitBannerView( + unit: .size320x100, + insets: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16), + onAdClick: { send(.adBannerClicked) } + ) + } else if let ad = store.withState({ state in + state.exploreItems.firstIndex(where: { $0.id == item.id }) + .flatMap { state.ad(after: $0) } + }) { + FeedAdRow( + ad: ad, + viewport: viewport.frame(in: .global), + onVisible: { send(.adVisible(code: ad.code, itemID: item.id)) }, + onAdClick: { send(.serverAdClicked(network: ad.network)) } + ) + .id(ad.code) + } } } + // 마지막 카드가 하단에 딱 붙지 않도록 10pt 여백. + .padding(.bottom, 10) } - // 마지막 카드가 하단에 딱 붙지 않도록 10pt 여백. - .padding(.bottom, 10) + .scrollIndicators(.hidden) + .scrollBounceBehavior(.basedOnSize, axes: .vertical) } - .scrollIndicators(.hidden) - .scrollBounceBehavior(.basedOnSize, axes: .vertical) - } - - /// 카드 사이에 끼우는 배너 광고 한 줄. - /// - /// 여백은 AdFitBannerView 내부에서 **광고가 실제로 노출될 때만** 적용된다(insets). - /// 광고가 없으면 여백까지 통째로 접혀 카드가 연속으로 이어진다. - /// 상하 12 는 위아래 카드의 vertical 패딩과 대칭을 이루고, 좌측 정렬로 카드 좌측 라인과 맞춘다. - @ViewBuilder - func adBannerRow() -> some View { - AdFitBannerView( - unit: .size320x100, - insets: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16), - onAdClick: { send(.adBannerClicked) } - ) } /// 좌우 스와이프로 카테고리 전환 (빈 상태/스켈레톤 포함 콘텐츠 영역 전체에 적용). diff --git a/Projects/Feature/Hifi/Tests/Sources/HifiTests.swift b/Projects/Feature/Hifi/Tests/Sources/HifiTests.swift index fb23bdb8..bc6448fd 100644 --- a/Projects/Feature/Hifi/Tests/Sources/HifiTests.swift +++ b/Projects/Feature/Hifi/Tests/Sources/HifiTests.swift @@ -1,26 +1,89 @@ -// -// HifiTests.swift -// Feature.HifiTests -// -// Created by Roy on 2026-06-04. -// - import Testing +import ComposableArchitecture +import AdDomainInterface @testable import Hifi struct HifiTests { + @Test + func serverAdsFollowEveryThreeItemsAfterKakaoSlot() { + var state = HifiFeature.State() + state.ads = [ad("first"), ad("second")] + #expect(state.ad(after: -1) == nil) + #expect(state.ad(after: 0) == nil) + #expect(state.ad(after: 2) == nil) + #expect(state.ad(after: 4) == nil) + #expect(state.ad(after: 5)?.code == "first") + #expect(state.ad(after: 8)?.code == "second") + #expect(state.ad(after: 11)?.code == "first") + #expect(state.ad(after: 14)?.code == "second") + #expect(state.ad(after: 12) == nil) + state.ads = [] + #expect(state.ad(after: 5) == nil) + } - @Test - func hifiExample() { - // This is an example of a test case. - #expect(true) + @Test @MainActor + func fetchingAdsDoesNotRecordImpressions() async { + let ads = [ad("first")] + let store = TestStore(initialState: HifiFeature.State()) { HifiFeature() } + await store.send(.inner(.adsResponse(.success(ads)))) { + $0.ads = ads } + #expect(store.state.reportedAdCodesByItemID.isEmpty) + await store.finish() + } + + @Test @MainActor + func unknownAndAlreadyReportedAdsDoNotRecordAgain() async { + var state = HifiFeature.State() + state.ads = [ad("first")] + state.reportedAdCodesByItemID = [6: ["first"]] + let store = TestStore(initialState: state) { HifiFeature() } + await store.send(.view(.adVisible(code: "unknown", itemID: 6))) + await store.send(.view(.adVisible(code: "first", itemID: 6))) + await store.finish() + } - @Test - func hifiLogicTest() { - let result = true - #expect(result == true) + @Test @MainActor + func visibleAdRecordsOncePerItem() async { + let client = ImpressionRecorder() + var state = HifiFeature.State() + state.ads = [ad("first")] + let store = TestStore(initialState: state) { HifiFeature() } withDependencies: { + $0.feedAdUseCase = client + } + await store.send(.view(.adVisible(code: "first", itemID: 6))) { + $0.reportedAdCodesByItemID = [6: ["first"]] } + await store.receive(\.inner.impressionResponse) + await store.send(.view(.adVisible(code: "first", itemID: 6))) + await store.send(.view(.adVisible(code: "first", itemID: 12))) { + $0.reportedAdCodesByItemID[12] = ["first"] + } + await store.receive(\.inner.impressionResponse) + let codes = await client.codes + #expect(codes == ["first", "first"]) + await store.finish() + } + private func ad(_ code: String) -> FeedAd { + FeedAd( + code: code, + network: "ADPICK", + title: "광고 제목", + subtitle: "광고 설명", + imageURL: "https://example.com/image.jpg", + ctaText: "구매하러 가기", + clickURL: "https://ad.picke.store/c/\(code)", + label: "광고" + ) + } } +private actor ImpressionRecorder: FeedAdInterface { + var codes: [String] = [] + + func fetchAds() async throws -> [FeedAd] { [] } + func recordImpressions(codes: [String]) async throws { + self.codes.append(contentsOf: codes) + } +} diff --git a/Projects/Feature/Profile/Project.swift b/Projects/Feature/Profile/Project.swift index 6b181aca..a135de81 100644 --- a/Projects/Feature/Profile/Project.swift +++ b/Projects/Feature/Profile/Project.swift @@ -24,9 +24,9 @@ let project = Project.makeModule( .domain(.auth, .interface), .domain(.battle, .interface), .domain(.notification, .interface), - .feature(.featureSharedUI, .implementation), // 마이페이지 하단 배너 광고 // 리워드 광고 계약(RewardedAdClient)은 Ad Interface 에서 온다. - .feature(.ad), + .feature(.ad, .implementation), + .feature(.ad, .interface), .SPM.composableArchitecture, .SPM.tcaFlow, .SPM.kingfisher, diff --git a/Projects/Feature/Profile/Sources/Main/Reducer/ProfileFeature.swift b/Projects/Feature/Profile/Sources/Main/Reducer/ProfileFeature.swift index b8be8465..247888b0 100644 --- a/Projects/Feature/Profile/Sources/Main/Reducer/ProfileFeature.swift +++ b/Projects/Feature/Profile/Sources/Main/Reducer/ProfileFeature.swift @@ -102,6 +102,7 @@ public struct ProfileFeature { case philosopherTapped case menuTapped(MenuItem) case adNativeClicked + case serverAdClicked(network: String) } public enum AsyncAction: Equatable { @@ -236,6 +237,14 @@ extension ProfileFeature { case let .menuTapped(item): return .send(.delegate(.menuSelected(item))) + case let .serverAdClicked(network): + analyticsUseCase.track(.adClick(AdClickData( + placement: .mypage, + format: .native, + unit: network + ))) + return .none + case .adNativeClicked: analyticsUseCase.track(.adClick(AdClickData(placement: .mypage, format: .native, unit: "ADFIT_NATIVE_2_1"))) return .none diff --git a/Projects/Feature/Profile/Sources/Main/View/ProfileView.swift b/Projects/Feature/Profile/Sources/Main/View/ProfileView.swift index 32fa4ef0..f9720fa9 100644 --- a/Projects/Feature/Profile/Sources/Main/View/ProfileView.swift +++ b/Projects/Feature/Profile/Sources/Main/View/ProfileView.swift @@ -5,7 +5,7 @@ import SwiftUI -import FeatureSharedUI +import Ad import ComposableArchitecture import PickeDesignKit import PickeSharedUI @@ -19,34 +19,39 @@ public struct ProfileView: View { } public var body: some View { - VStack(spacing: 0) { - topBar() - - if store.viewState == .loading { - ProfileSkeletonView() - } else { - // xr63n: 카드 그룹 ↔ 메뉴 그룹 gap 20 - VStack(spacing: 20) { - // T3oil: 카드 3개 gap 16, 좌우 16 - VStack(spacing: 16) { - profileCard() - chargeButton() - philosopherCard() - } - .padding(.horizontal, 16) - - // HFFUM: 메뉴 리스트 좌우 16 - menuList() + GeometryReader { viewport in + VStack(spacing: 0) { + topBar() + + if store.viewState == .loading { + ProfileSkeletonView() + } else { + // xr63n: 카드 그룹 ↔ 메뉴 그룹 gap 20 + VStack(spacing: 20) { + // T3oil: 카드 3개 gap 16, 좌우 16 + VStack(spacing: 16) { + profileCard() + chargeButton() + philosopherCard() + } .padding(.horizontal, 16) - Spacer(minLength: 0) - - // 마이페이지 하단 네이티브 광고 — 2:1(.wide) 규격, 좌우 여백 16. - AdFitNativeAdView( - unit: .wide, - insets: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16), - onAdClick: { send(.adNativeClicked) } - ) + // HFFUM: 메뉴 리스트 좌우 16 + menuList() + .padding(.horizontal, 16) + + Spacer(minLength: 0) + + // 마이페이지 하단 광고 — 서버 광고와 Kakao 광고를 번갈아 노출한다. + MixedNativeAdView( + unit: .wide, + insets: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16), + placementKey: "mixedNativeAd.profile", + viewport: viewport.frame(in: .global), + onAdClick: { send(.adNativeClicked) }, + onServerAdClick: { send(.serverAdClicked(network: $0)) } + ) + } } } } diff --git a/Projects/Service/API/Sources/Ads/AdsAPI.swift b/Projects/Service/API/Sources/Ads/AdsAPI.swift new file mode 100644 index 00000000..bd832219 --- /dev/null +++ b/Projects/Service/API/Sources/Ads/AdsAPI.swift @@ -0,0 +1,15 @@ +import Foundation + +public enum AdsAPI: String, CaseIterable { + case ads + case impressions + + public var description: String { + switch self { + case .ads: + return "" + case .impressions: + return "/impressions" + } + } +} diff --git a/Projects/Service/API/Sources/Base/PieckeDomain.swift b/Projects/Service/API/Sources/Base/PieckeDomain.swift index 42e94c6e..23ef89bd 100644 --- a/Projects/Service/API/Sources/Base/PieckeDomain.swift +++ b/Projects/Service/API/Sources/Base/PieckeDomain.swift @@ -21,6 +21,7 @@ public enum PieckeDomain { case search case notification case device + case ads } extension PieckeDomain: PickeDomainType { @@ -52,6 +53,8 @@ extension PieckeDomain: PickeDomainType { return "api/v1/notifications" case .device: return "api/v1/devices" + case .ads: + return "api/v1/ads" } } } diff --git a/Projects/Service/APIEndpoint/Sources/Ad/AdsService.swift b/Projects/Service/APIEndpoint/Sources/Ad/AdsService.swift new file mode 100644 index 00000000..2ee95277 --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Ad/AdsService.swift @@ -0,0 +1,36 @@ +import Foundation +import Alamofire +import API +import PickeNetwork + +public enum AdsService { + case list(query: AdsQueryRequest) + case impressions(body: AdsImpressionsRequest) +} + +extension AdsService: PickeDataRequest { + public var domain: any PickeDomainType { PieckeDomain.ads } + + public var path: String { + switch self { + case .list: + return AdsAPI.ads.description + case .impressions: + return AdsAPI.impressions.description + } + } + + public var method: HTTPMethod { + switch self { + case .list: return .get + case .impressions: return .post + } + } + + public var parameters: (any Encodable & Sendable)? { + switch self { + case let .list(query): return query + case let .impressions(body): return body + } + } +} diff --git a/Projects/Service/APIEndpoint/Sources/Ad/Request/AdSlot.swift b/Projects/Service/APIEndpoint/Sources/Ad/Request/AdSlot.swift new file mode 100644 index 00000000..6e1dd41c --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Ad/Request/AdSlot.swift @@ -0,0 +1,3 @@ +public enum AdSlot: String, Encodable, Sendable { + case homeFeed = "HOME_FEED" +} diff --git a/Projects/Service/APIEndpoint/Sources/Ad/Request/AdsImpressionsRequest.swift b/Projects/Service/APIEndpoint/Sources/Ad/Request/AdsImpressionsRequest.swift new file mode 100644 index 00000000..bcff1fb9 --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Ad/Request/AdsImpressionsRequest.swift @@ -0,0 +1,9 @@ +import Foundation + +public struct AdsImpressionsRequest: Encodable, Sendable { + public let codes: [String] + + public init(codes: [String]) { + self.codes = codes + } +} diff --git a/Projects/Service/APIEndpoint/Sources/Ad/Request/AdsQueryRequest.swift b/Projects/Service/APIEndpoint/Sources/Ad/Request/AdsQueryRequest.swift new file mode 100644 index 00000000..8903ef69 --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Ad/Request/AdsQueryRequest.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct AdsQueryRequest: Encodable, Sendable { + public let slot: AdSlot + public let os: String + public let size: Int + + public init( + slot: AdSlot = .homeFeed, + os: String = "IOS", + size: Int = 20 + ) { + self.slot = slot + self.os = os + self.size = size + } +} diff --git a/README.md b/README.md index 75ecd7d5..0f641472 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,11 @@ SwiftUI와 The Composable Architecture를 기반으로 한 Clean Architecture ~~~text Projects/ -├── App/ # 앱 진입점, AppReducer, DI 조립, 리소스 +├── App/ # 앱 진입점, 스플래시, AppReducer, DI 조립, 리소스 ├── Feature/ │ ├── FeatureAssembly/ # 전체 Feature 조립 결과를 앱에 제공 │ ├── FeatureSharedUI/ # Feature 공통 UI +│ ├── Ad/ # Kakao·서버 광고 표시 │ ├── Auth/ # 로그인 │ ├── Home/ # 홈·출석 모달 │ ├── Battle/ # 배틀 메인 @@ -45,10 +46,10 @@ Projects/ │ ├── Hifi/ # 탐색·검색 │ ├── Notification/ # 알림 │ ├── Profile/ # 마이페이지·설정·리캡 -│ ├── Splash/ # 스플래시·앱 업데이트 │ └── Web/ # WebView ├── Domain/ │ ├── DomainAssembly/ # 도메인별 라이브 의존성 조립 +│ ├── AdDomain/ # 광고 모델·조회·노출 집계 (Repository/UseCase) │ ├── AuthDomain/ # 인증 도메인 │ ├── BattleDomain/ # 배틀 도메인 │ ├── CommentDomain/ # 댓글·대댓글 도메인 @@ -125,59 +126,498 @@ flowchart TD - Feature는 Repository 구현체를 직접 알지 않고 UseCase 또는 Interface만 사용합니다. - 테스트·프리뷰 기본값은 Interface 또는 Testing 타깃에서 관리합니다. + ## 모듈 그래프 -~~~bash -./make graph # 외부 패키지·Demo를 제외하고 Tests·Testing·Interface를 포함한 모듈 그래프 -./make graph:prod # 외부 패키지·Demo·Tests를 제외한 제품 그래프 -~~~ +현재 42개 모듈의 구현·Interface 타깃 의존성을 표시합니다. 각 항목을 펼치면 GitHub에서 SVG 그림을 바로 볼 수 있습니다. + +화살표는 **참조하는 타깃 → 참조되는 타깃**, 점선 테두리는 **Interface**입니다. `Project.swift`의 `dependencies`·`interfaceDependencies`와 템플릿이 연결하는 자기 Interface를 반영합니다. 외부 SPM 패키지는 이름으로 별도 표기하고, Tests·Testing·Demo와 전이 의존성은 생략합니다. + +`APIEndpoint → AuthDomainInterface`처럼 현재 코드에 존재하는 계층 간 참조도 그대로 표시합니다. 실행 순서나 이상적인 아키텍처를 나타내는 그림은 아닙니다. + +[광고 HTML](docs/diagrams/picke-ads.html) · [전체 도메인 HTML](docs/diagrams/picke-domains.html) — 파일을 내려받아 브라우저에서 열면 확대·검색할 수 있습니다. + +### App · 1개 + +
+Picke + +[모듈 선언](Projects/App/Project.swift) + +![Picke 직접 의존 관계](docs/diagrams/modules/Picke.svg) + +외부 패키지 선언: `googleMobileAds`, `kingfisher`. + +
+ +### Feature · 11개 + +
+Ad + +[모듈 선언](Projects/Feature/Ad/Project.swift) + +![Ad 직접 의존 관계](docs/diagrams/modules/Ad.svg) -TuistSpider에서 Picke와 주요 조립 모듈을 기준으로 내부 의존성을 확장한 그래프입니다. 외부 의존성은 숨기고, 의존하는 방향을 전체 깊이로 표시했습니다. +외부 패키지 선언: `adFit`, `composableArchitecture`, `googleMobileAds`. + +
+ +
+Auth + +[모듈 선언](Projects/Feature/Auth/Project.swift) + +![Auth 직접 의존 관계](docs/diagrams/modules/Auth.svg) + +외부 패키지 선언: `composableArchitecture`. + +
-Picke 전체 모듈 +Battle -![Picke 전체 모듈 단계별 그래프](docs/grpah/Picke-grouped-Picke.png) +[모듈 선언](Projects/Feature/Battle/Project.swift) -![Picke 전체 모듈 그래프](docs/grpah/Picke-expanded-Picke.png) +![Battle 직접 의존 관계](docs/diagrams/modules/Battle.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+Chat + +[모듈 선언](Projects/Feature/Chat/Project.swift) + +![Chat 직접 의존 관계](docs/diagrams/modules/Chat.svg) + +외부 패키지 선언: `composableArchitecture`, `tcaFlow`.
FeatureAssembly -![FeatureAssembly 모듈 그래프](docs/grpah/Picke-expanded-FeatureAssembly.png) +[모듈 선언](Projects/Feature/FeatureAssembly/Project.swift) + +![FeatureAssembly 직접 의존 관계](docs/diagrams/modules/FeatureAssembly.svg) + +
+ +
+FeatureSharedUI + +[모듈 선언](Projects/Feature/FeatureSharedUI/Project.swift) + +![FeatureSharedUI 직접 의존 관계](docs/diagrams/modules/FeatureSharedUI.svg) + +외부 패키지 선언: `adFit`. + +
+ +
+Hifi + +[모듈 선언](Projects/Feature/Hifi/Project.swift) + +![Hifi 직접 의존 관계](docs/diagrams/modules/Hifi.svg) + +외부 패키지 선언: `composableArchitecture`, `kingfisher`. + +
+ +
+Home + +[모듈 선언](Projects/Feature/Home/Project.swift) + +![Home 직접 의존 관계](docs/diagrams/modules/Home.svg) + +외부 패키지 선언: `composableArchitecture`, `kingfisher`, `tcaFlow`. + +
+ +
+Notification + +[모듈 선언](Projects/Feature/Notification/Project.swift) + +![Notification 직접 의존 관계](docs/diagrams/modules/Notification.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+Profile + +[모듈 선언](Projects/Feature/Profile/Project.swift) + +![Profile 직접 의존 관계](docs/diagrams/modules/Profile.svg) + +외부 패키지 선언: `composableArchitecture`, `kingfisher`, `tcaFlow`. + +
+ +
+Web + +[모듈 선언](Projects/Feature/Web/Project.swift) + +![Web 직접 의존 관계](docs/diagrams/modules/Web.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +### Domain · 12개 + +
+AdDomain + +[모듈 선언](Projects/Domain/AdDomain/Project.swift) + +![AdDomain 직접 의존 관계](docs/diagrams/modules/AdDomain.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+AppUpdateDomain + +[모듈 선언](Projects/Domain/AppUpdateDomain/Project.swift) + +![AppUpdateDomain 직접 의존 관계](docs/diagrams/modules/AppUpdateDomain.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+AttendanceDomain + +[모듈 선언](Projects/Domain/AttendanceDomain/Project.swift) + +![AttendanceDomain 직접 의존 관계](docs/diagrams/modules/AttendanceDomain.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+AuthDomain + +[모듈 선언](Projects/Domain/AuthDomain/Project.swift) + +![AuthDomain 직접 의존 관계](docs/diagrams/modules/AuthDomain.svg) + +외부 패키지 선언: `composableArchitecture`, `googleSignIn`, `sharing`. + +
+ +
+BattleDomain + +[모듈 선언](Projects/Domain/BattleDomain/Project.swift) + +![BattleDomain 직접 의존 관계](docs/diagrams/modules/BattleDomain.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+CommentDomain + +[모듈 선언](Projects/Domain/CommentDomain/Project.swift) + +![CommentDomain 직접 의존 관계](docs/diagrams/modules/CommentDomain.svg) + +외부 패키지 선언: `composableArchitecture`.
DomainAssembly -![DomainAssembly 모듈 그래프](docs/grpah/Picke-expanded-DomainAssembly.png) +[모듈 선언](Projects/Domain/DomainAssembly/Project.swift) + +![DomainAssembly 직접 의존 관계](docs/diagrams/modules/DomainAssembly.svg) + +
+ +
+HomeDomain + +[모듈 선언](Projects/Domain/HomeDomain/Project.swift) + +![HomeDomain 직접 의존 관계](docs/diagrams/modules/HomeDomain.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+NotificationDomain + +[모듈 선언](Projects/Domain/NotificationDomain/Project.swift) + +![NotificationDomain 직접 의존 관계](docs/diagrams/modules/NotificationDomain.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+PerspectiveDomain + +[모듈 선언](Projects/Domain/PerspectiveDomain/Project.swift) + +![PerspectiveDomain 직접 의존 관계](docs/diagrams/modules/PerspectiveDomain.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+ProfileDomain + +[모듈 선언](Projects/Domain/ProfileDomain/Project.swift) + +![ProfileDomain 직접 의존 관계](docs/diagrams/modules/ProfileDomain.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+SearchDomain + +[모듈 선언](Projects/Domain/SearchDomain/Project.swift) + +![SearchDomain 직접 의존 관계](docs/diagrams/modules/SearchDomain.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +### Service · 8개 + +
+API + +[모듈 선언](Projects/Service/API/Project.swift) + +![API 직접 의존 관계](docs/diagrams/modules/API.svg) + +
+ +
+APIEndpoint + +[모듈 선언](Projects/Service/APIEndpoint/Project.swift) + +![APIEndpoint 직접 의존 관계](docs/diagrams/modules/APIEndpoint.svg) + +외부 패키지 선언: `alamofire`. + +
+ +
+AudioPlayerService + +[모듈 선언](Projects/Service/AudioPlayerService/Project.swift) + +![AudioPlayerService 직접 의존 관계](docs/diagrams/modules/AudioPlayerService.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+DeviceService + +[모듈 선언](Projects/Service/DeviceService/Project.swift) + +![DeviceService 직접 의존 관계](docs/diagrams/modules/DeviceService.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+PickeAnalytics + +[모듈 선언](Projects/Service/PickeAnalytics/Project.swift) + +![PickeAnalytics 직접 의존 관계](docs/diagrams/modules/PickeAnalytics.svg) + +외부 패키지 선언: `composableArchitecture`, `mixpanel`, `mixpanelSessionReplay`, `sentry`, `sentrySwiftUI`. + +
+ +
+PickeAuth + +[모듈 선언](Projects/Service/PickeAuth/Project.swift) + +![PickeAuth 직접 의존 관계](docs/diagrams/modules/PickeAuth.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+PickeConfig + +[모듈 선언](Projects/Service/PickeConfig/Project.swift) + +![PickeConfig 직접 의존 관계](docs/diagrams/modules/PickeConfig.svg) + +외부 패키지 선언: `firebaseCrashlytics`. + +다른 내부 모듈에 대한 직접 의존성이 없습니다.
ServiceAssembly -![ServiceAssembly 모듈 그래프](docs/grpah/Picke-expanded-ServiceAssembly.png) +[모듈 선언](Projects/Service/ServiceAssembly/Project.swift) + +![ServiceAssembly 직접 의존 관계](docs/diagrams/modules/ServiceAssembly.svg)
+### Core · 7개 +
CoreAssembly -![CoreAssembly 모듈 그래프](docs/grpah/Picke-expanded-CoreAssembly.png) +[모듈 선언](Projects/Core/CoreAssembly/Project.swift) + +![CoreAssembly 직접 의존 관계](docs/diagrams/modules/CoreAssembly.svg) + +외부 패키지 선언: `composableArchitecture`. + +
+ +
+PickeCoreLogger + +[모듈 선언](Projects/Core/PickeCoreLogger/Project.swift) + +![PickeCoreLogger 직접 의존 관계](docs/diagrams/modules/PickeCoreLogger.svg) + +다른 내부 모듈에 대한 직접 의존성이 없습니다. + +
+ +
+PickeCoreUI + +[모듈 선언](Projects/Core/PickeCoreUI/Project.swift) + +![PickeCoreUI 직접 의존 관계](docs/diagrams/modules/PickeCoreUI.svg) + +다른 내부 모듈에 대한 직접 의존성이 없습니다. + +
+ +
+PickeCoreUtility + +[모듈 선언](Projects/Core/PickeCoreUtility/Project.swift) + +![PickeCoreUtility 직접 의존 관계](docs/diagrams/modules/PickeCoreUtility.svg) + +외부 패키지 선언: `dependencies`. + +
+ +
+PickeNetwork + +[모듈 선언](Projects/Core/PickeNetwork/Project.swift) + +![PickeNetwork 직접 의존 관계](docs/diagrams/modules/PickeNetwork.svg) + +외부 패키지 선언: `alamofire`, `dependencies`. + +
+ +
+PickeStorage + +[모듈 선언](Projects/Core/PickeStorage/Project.swift) + +![PickeStorage 직접 의존 관계](docs/diagrams/modules/PickeStorage.svg) + +외부 패키지 선언: `composableArchitecture`, `sharing`, `sqliteData`. + +
+ +
+PickeThirdParty + +[모듈 선언](Projects/Core/PickeThirdParty/Project.swift) + +![PickeThirdParty 직접 의존 관계](docs/diagrams/modules/PickeThirdParty.svg) + +외부 패키지 선언: `composableArchitecture`, `sdwebImage`, `tcaFlow`. + +
+ +### UI · 3개 + +
+PickeAnimation + +[모듈 선언](Projects/UI/PickeAnimation/Project.swift) + +![PickeAnimation 직접 의존 관계](docs/diagrams/modules/PickeAnimation.svg) + +외부 패키지 선언: `sdwebImageCore`. + +다른 내부 모듈에 대한 직접 의존성이 없습니다. + +
+ +
+PickeDesignKit + +[모듈 선언](Projects/UI/PickeDesignKit/Project.swift) + +![PickeDesignKit 직접 의존 관계](docs/diagrams/modules/PickeDesignKit.svg) + +외부 패키지 선언: `composableArchitecture`.
PickeSharedUI -![PickeSharedUI 모듈 그래프](docs/grpah/Picke-expanded-PickeSharedUI.png) +[모듈 선언](Projects/UI/PickeSharedUI/Project.swift) + +![PickeSharedUI 직접 의존 관계](docs/diagrams/modules/PickeSharedUI.svg) + +외부 패키지 선언: `composableArchitecture`, `kingfisher`.
+갱신·검증: + +```bash +python3 scripts/generate_module_diagrams.py +python3 scripts/generate_module_diagrams.py --check +``` + +SVG 생성에는 Graphviz의 `dot`이 필요합니다. 앱 빌드나 Tuist 캐시 생성은 실행하지 않습니다. + + + ## 기술 스택 | 영역 | 기술 | @@ -242,32 +682,31 @@ OAuth redirect URI는 서버 중계 흐름을 기준으로 등록합니다. ## Tuist Dashboard와 캐시 -이 저장소는 로컬 개발에서만 Tuist Dashboard 프로젝트 `picke2026/picke`를 사용합니다. 일반 generate/project 확인은 Dashboard에 연결해 메트릭을 남기고, 바이너리 캐시 warm은 로컬 저장소만 사용합니다. CI에서는 대시보드 연결과 캐시 준비를 비활성화하며, 별도 CI 캐시 연동 설정은 추가하지 않습니다. +Tuist Dashboard 프로젝트는 `picke2026/picke`입니다. 모듈 캐시 프로필, 저장소, Xcode 컴파일 캐시와 업로드 정책의 기준은 [Tuist.swift](Tuist.swift)입니다. -[Tuist.swift](Tuist.swift)의 기준 설정: +`TuistTool.swift`는 install 이후 외부 모듈 캐시를 준비하고, CI 또는 `--no-binary-cache` 옵션에서는 이 준비 단계를 생략합니다. `generate`는 별도 인증 명령이나 캐시 비활성화 옵션을 추가하지 않고 전달받은 인자로 실행합니다. -- 로컬 generate/project show: `fullHandle = "picke2026/picke"`, 기본 모듈 캐시 프로필 `.onlyExternal` -- 로컬 cache warm: `TUIST_LOCAL_CACHE_ONLY=true` 환경에서만 `fullHandle = nil`, 기본 모듈 캐시 프로필 `.onlyExternal` -- CI: `fullHandle = nil`, 기본 모듈 캐시 프로필 `.none` -- Xcode 컴파일 캐시: 현재 Explicit Modules를 끈 빌드 설정과 호환되지 않아 `enableCaching = false`, `cache.upload = false`로 비활성화 -- 인증: `optionalAuthentication = true`로 설정해 로그인되지 않은 환경에서도 generate가 실패하지 않도록 유지 +`./make`가 프로젝트 명령의 단일 진입점이며 `TuistTool.swift`를 실행합니다. 캐시 준비·사용·CI 제외·캐시 비활성화 옵션을 이 실행 경로에서 처리합니다. 소스를 수정하면 실행 파일을 다시 컴파일하지 않아도 다음 실행에 반영됩니다. -로컬에서 Dashboard 연결과 로컬 바이너리 캐시를 준비하려면 `./make setup`을 실행합니다. 이 명령은 mise 도구 설치 후 Tuist 로그인을 확인하고, 로그인되어 있지 않으면 `tuist auth login`을 실행한 뒤 `Tuist.swift`의 `picke2026/picke` 연결을 `tuist project show`로 확인합니다. 이후 의존성을 설치하고 외부 모듈 바이너리 캐시를 로컬 저장소에 준비한 다음 프로젝트를 생성합니다. - -~~~bash -./make setup -~~~ +```bash +./make install --no-open # 의존성 설치 → 로컬 외부 캐시 준비 → 프로젝트 생성 +./make generate --no-open # 준비된 캐시를 사용해 프로젝트 생성 +./make cache # 로컬 외부 바이너리 캐시 준비 +./make cache:setup # Xcode Compilation Cache 설정 +./make setup # mise 설치 → install → cache warm → generate +``` -`./make generate`, `./make test`, `./make cache`도 로컬에서는 필요한 Tuist Dashboard 인증과 프로젝트 확인을 먼저 수행합니다. 바이너리 캐시를 쓰지 않을 때는 Tuist 옵션을 그대로 전달합니다. +`generate`는 캐시를 새로 빌드하지 않습니다. 첫 실행이나 의존성 변경 후에는 `install` 또는 `cache`로 준비합니다. 캐시 적중은 Xcode 버전, 구성과 의존성 해시가 일치해야 합니다. -~~~bash +```bash +./make install --no-binary-cache --no-open # 캐시 준비와 사용 모두 생략 ./make generate --no-binary-cache --no-open ./make test --no-binary-cache -~~~ +``` -`./make cache`와 `./make cache:setup`은 기본적으로 `TUIST_LOCAL_CACHE_ONLY=true tuist cache warm --external-only`를 실행해 외부 의존성 중심으로 로컬 캐시를 데웁니다. CI에서는 Dashboard 인증, 프로젝트 확인, 캐시 준비를 건너뛰며, 별도 CI 캐시 연동은 하지 않습니다. +`./make cache`는 `tuist cache warm --external-only`를 실행합니다. `TUIST_LOCAL_CACHE_ONLY=true` 환경변수를 함께 전달하며, 실제 저장소·연결 정책은 `Tuist.swift`가 결정합니다. 캐시 준비가 실패하면 오류를 반환하고 generate로 넘어가지 않습니다. `./make cache:setup`은 Attendance와 동일하게 `tuist setup cache`를 실행하는 별도 명령이며, `Tuist.swift`의 컴파일 캐시 설정과 함께 사용합니다. -Fastlane이나 CI용 래퍼처럼 캐시가 필요 없는 자동화 경로에서는 `tuist generate --no-binary-cache --no-open` 형태로 실행합니다. +명령 순서와 CI/opt-out 동작은 `python3 scripts/tests/test_tuist_cache_commands.py`로 검사합니다. 이 검사는 실제 패키지 설치나 캐시 빌드를 실행하지 않습니다. ## 빠른 시작 @@ -290,23 +729,23 @@ ln -s AGENTS.md CLAUDE.md ## 개발 명령어 ~~~bash -./make setup # mise 설치, Dashboard 확인, install, 외부 캐시 준비, generate +./make setup # mise 설치, install, 로컬 외부 캐시 준비, generate ./make generate # Demo 앱을 포함해 Xcode 프로젝트 생성 ./make generate --no-open # Xcode를 열지 않고 프로젝트 생성 ./make generate --no-binary-cache --no-open ./make build # clean, install, generate -./make install # 의존성 설치 후 generate +./make install # 의존성 설치, 로컬 외부 캐시 준비 후 generate ./make test # 전체 테스트 ./make test --no-binary-cache # 로컬 바이너리 캐시 없이 전체 테스트 ./make cache # 외부 바이너리 캐시 준비 -./make cache:setup # 외부 바이너리 캐시 준비(cache 별칭) +./make cache:setup # Xcode Compilation Cache 설정 ./make format # SwiftFormat 적용 ./make lint # SwiftFormat 검사 ./make clean # 생성 프로젝트 정리 ./make reset # DerivedData 정리 후 프로젝트 재생성 ~~~ -훅 호환용 `make test`는 실제 테스트를 실행하지 않고 스킵 메시지만 출력합니다. 실제 테스트는 `./make test` 또는 `mise exec -- tuist test`를 사용합니다. +`Makefile`은 사용하지 않습니다. 테스트는 `./make test` 또는 `mise exec -- tuist test`로 실행합니다. 새 모듈 생성: diff --git a/Tuist.swift b/Tuist.swift index 67c4e8e5..cb52cb85 100644 --- a/Tuist.swift +++ b/Tuist.swift @@ -6,8 +6,10 @@ let usesLocalCacheOnly = ProcessInfo.processInfo.environment["TUIST_LOCAL_CACHE_ let tuist = Tuist( // 일반 로컬 generate/project show 는 Dashboard 에 연결하고, // cache warm 프로세스만 로컬 저장소를 쓰도록 handle 을 비운다. - fullHandle: Environment.isCI || usesLocalCacheOnly ? nil : "picke2026/picke", - cache: .cache(upload: false), + fullHandle: "picke2026/picke", + xcodeCache: .xcodeCache( + upload: true + ), project: .tuist( compatibleXcodeVersions: .all, swiftVersion: .some("6.0.0"), @@ -17,22 +19,13 @@ let tuist = Tuist( .local(path: .relativeToRoot("Plugins/DependencyPlugin")), ], generationOptions: .options( - // 🔒 패키지 버전 잠금 비활성화 여부 (기본 false) - // true = Package.resolved 고정 무시(최신으로 다시 풀기) - // false = 기존 잠금 유지(권장) - disablePackageVersionLocking: false, - - // ⚠️ 사이드 이펙트(스크립트 등) 경고를 어떤 타겟에 표시할지 - // .all / .selected([...]) / .none staticSideEffectsWarningTargets: .all, optionalAuthentication: true, - // 현재 Explicit Modules를 끈 빌드 설정에서는 Xcode 컴파일 캐시를 사용할 수 없다. - // 로컬 개발은 아래의 외부 모듈 바이너리 캐시를 사용한다. - enableCaching: false + enableCaching: true ), installOptions: .options(), cacheOptions: .options( - profiles: .profiles(default: Environment.isCI ? .none : .onlyExternal) + profiles: .profiles(default: .onlyExternal) ) ) ) diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved index 606051b6..f96a4348 100644 --- a/Tuist/Package.resolved +++ b/Tuist/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ed04bf35e3364b25fd627c02cb1852f827813d3f714fe41fa18bdc43204c0bea", + "originHash" : "f600a9f05ebc86a1d19a3a1a0c371a61f3bd90feaf48f4866e663ecb77714bb9", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire.git", "state" : { - "revision" : "3f99050e75bbc6fe71fc323adabb039756680016", - "version" : "5.11.1" + "revision" : "bda9ed57d72988a3a2ada33d824583541f86eac6", + "version" : "5.12.2" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", - "version" : "11.2.0" + "revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902", + "version" : "11.3.1" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/openid/AppAuth-iOS.git", "state" : { - "revision" : "145104f5ea9d58ae21b60add007c33c1cc0c948e", - "version" : "2.0.0" + "revision" : "a7caeda164dc5108bf4649472b28a5af65dc6f33", + "version" : "2.1.0" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "5928286acce13def418ec36d05a001a9641086f2", - "version" : "1.0.3" + "revision" : "114354e8c1667a2edc4993700fb9fa4f90157b56", + "version" : "1.2.2" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk", "state" : { - "revision" : "d47760f97a853808be6a045d278fbd12abf546b6", - "version" : "12.12.0" + "revision" : "cf44bf2fa90b1dbba999ee2bb4dce4eaf7bceae8", + "version" : "12.19.1" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", "state" : { - "revision" : "23fa6970874ec8f3ac0039b3778ca8d6545d50ee", - "version" : "3.4.2" + "revision" : "0208e2681ec81f8a9c81084696f60fcce26cb7ee", + "version" : "3.7.0" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "1657f705bc70255ff9d66dfcc697a85db164998f", - "version" : "12.11.0" + "revision" : "8fe40b69bd53241847814422101188465f7ff728", + "version" : "12.19.0" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleDataTransport.git", "state" : { - "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", - "version" : "10.1.0" + "revision" : "ba3358d3c3dbae8ef230b58a46b97ad65e84e974", + "version" : "10.1.1" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleSignIn-iOS", "state" : { - "revision" : "913b4005ea26aebe1c97d54e35ad82a515924c71", - "version" : "9.1.0" + "revision" : "08d8dcecafb575f98879ffdbb8302c1b9ad65d19", + "version" : "9.2.0" } }, { @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", - "version" : "8.1.0" + "revision" : "92c8f6dc3ac375d6febdfcb3db68bc3d10633db3", + "version" : "8.1.3" } }, { @@ -159,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Kingfisher.git", "state" : { - "revision" : "cf8be20d07654570554c8a8a4952bc8a5766a8b0", - "version" : "8.9.0" + "revision" : "be0d257b9bd47a4e6e1265fc8c237411825f107a", + "version" : "8.12.0" } }, { @@ -195,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/nanopb.git", "state" : { - "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", - "version" : "2.30910.0" + "revision" : "3851d94a41890dea16dc3db34caf60e585cb4163", + "version" : "2.30910.1" } }, { @@ -204,8 +204,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/promises.git", "state" : { - "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", - "version" : "2.4.0" + "revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837", + "version" : "2.4.1" } }, { @@ -240,8 +240,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "revision" : "53eb9bd5da18e208cfd80e86863d3f4c7ba21b1d", - "version" : "9.21.0" + "revision" : "61e8cb02434b26fb34c58126d883e5663cfde238", + "version" : "9.28.0" } }, { @@ -267,8 +267,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", - "version" : "1.0.6" + "revision" : "82440fa0a8b1c381a6d1e8e0fc7bbba53ec63204", + "version" : "1.1.1" } }, { @@ -276,8 +276,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", - "version" : "1.3.0" + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version" : "1.6.0" } }, { @@ -303,14 +303,14 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", - "version" : "1.3.3" + "revision" : "4fb9e7cbb4b8f5a7354005db39447f570d6b3e19", + "version" : "1.7.3" } }, { "identity" : "swift-dependencies", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-dependencies.git", + "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { "revision" : "706feb7858a7f6c242879d137b8ee30926aa5b26", "version" : "1.12.0" @@ -330,8 +330,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-navigation", "state" : { - "revision" : "32f35241b8be0719c4c7f00eb27713b1cadb6248", - "version" : "2.8.0" + "revision" : "7e95e5e9ff0a64f6ad8bca59083f8d6ba381b6ff", + "version" : "2.11.2" } }, { @@ -357,8 +357,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "de219a1cf34e958134e75a9ebb134cf09bf52fc6", - "version" : "2.0.11" + "revision" : "597afd46249f4a71885b95d26b26cc5b893daf32", + "version" : "2.0.12" } }, { @@ -366,8 +366,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-sharing", "state" : { - "revision" : "8244fe63bf43e58188ab13851ad693eecf6a9e90", - "version" : "2.9.1" + "revision" : "3552faf8a6c18ce896ec5a72bd8c34755f0f03e0", + "version" : "2.10.1" } }, { @@ -384,8 +384,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "7cdc5c4514e24b1b828fd39f5b97badb7ffeaeae", - "version" : "0.37.0" + "revision" : "8a733f414adc1224c5185c3a35c30ee1ef217171", + "version" : "0.36.0" } }, { @@ -402,8 +402,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Roy-wonji/TCAFlow.git", "state" : { - "revision" : "4748aa2f56d50b0965e50575e4e332111085a91b", - "version" : "1.1.8" + "branch" : "main", + "revision" : "1b042061985c86b8fe357f51671fc88529261b8c" } }, { diff --git a/Tuist/Package.swift b/Tuist/Package.swift index d65b95f9..cab92469 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -85,7 +85,9 @@ let package = Package( // 소스 빌드와 바이너리 캐시가 같은 그래프를 쓰도록 마지막 비조건부 버전을 고정한다. .package(url: "https://github.com/pointfreeco/swift-dependencies", exact: "1.12.0"), .package(url: "https://github.com/pointfreeco/sqlite-data", exact: "1.11.0"), - .package(url: "https://github.com/Roy-wonji/TCAFlow.git", exact: "1.1.8"), + // SQLiteData 1.11.0의 section API가 요구하는 Select.From: Table 제약을 유지한다. + .package(url: "https://github.com/pointfreeco/swift-structured-queries", exact: "0.36.0"), + .package(url: "https://github.com/Roy-wonji/TCAFlow.git", branch: "main"), .package(url: "https://github.com/google/GoogleSignIn-iOS", from: "9.1.0"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.10.2"), .package(url: "https://github.com/openid/AppAuth-iOS.git", from: "2.0.0"), diff --git a/TuistTool.swift b/TuistTool.swift index 210d48d2..344566d6 100755 --- a/TuistTool.swift +++ b/TuistTool.swift @@ -19,10 +19,6 @@ private enum Command: String { case lint case clean case reset - case edit - case inspect - case inspectImports = "inspect-imports" - case inspectCoverage = "inspect-coverage" case module case moduleInit = "moduleinit" case feature @@ -76,9 +72,6 @@ private func runTuist( ) } -private var didPrepareLocalTuistAccess = false -private var isLocalTuistAccessUnavailable = false - private var isCIEnvironment: Bool { let environment = ProcessInfo.processInfo.environment let ciValues = ["1", "true", "TRUE"] @@ -92,56 +85,6 @@ private func usesBinaryCache(forwardedArguments: [String]) -> Bool { return !forwardedArguments.contains("--no-binary-cache") } -private func warnAndContinue(_ message: String) { - FileHandle.standardError.write(Data("⚠️ \(message)\n".utf8)) -} - -private func prepareLocalTuistAccess(allowFailure: Bool) -> Int32 { - guard !isCIEnvironment else { - print("CI 환경이라 Tuist Dashboard 인증과 프로젝트 확인을 건너뜁니다.") - return 0 - } - guard !didPrepareLocalTuistAccess else { return 0 } - - let whoamiStatus = runTuist(arguments: ["auth", "whoami"]) - if whoamiStatus != 0 { - let loginStatus = runTuist(arguments: ["auth", "login"]) - guard loginStatus == 0 else { - if allowFailure { - isLocalTuistAccessUnavailable = true - warnAndContinue("Tuist 인증에 실패했습니다. 바이너리 캐시 없이 계속 진행합니다.") - return 0 - } - return loginStatus - } - } - - let projectStatus = runTuist(arguments: ["project", "show"]) - guard projectStatus == 0 else { - if allowFailure { - isLocalTuistAccessUnavailable = true - warnAndContinue("Tuist Dashboard 프로젝트 확인에 실패했습니다. 바이너리 캐시 없이 계속 진행합니다.") - return 0 - } - return projectStatus - } - - didPrepareLocalTuistAccess = true - isLocalTuistAccessUnavailable = false - return 0 -} - -private func prepareBinaryCacheIfNeeded( - forwardedArguments: [String], - allowFailure: Bool -) -> Int32 { - guard usesBinaryCache(forwardedArguments: forwardedArguments) else { - print("--no-binary-cache 옵션이 있어 Tuist Dashboard 인증과 캐시 준비를 건너뜁니다.") - return 0 - } - return prepareLocalTuistAccess(allowFailure: allowFailure) -} - private func cacheWarmArguments(forwardedArguments: [String]) -> [String] { if forwardedArguments.first == "print-hashes" { return ["cache"] + forwardedArguments @@ -171,14 +114,6 @@ private func filteredGenerateArguments(forwardedArguments: [String]) -> [String] } } -private func generateArguments(forwardedArguments: [String]) -> [String] { - guard isLocalTuistAccessUnavailable, usesBinaryCache(forwardedArguments: forwardedArguments) - else { - return forwardedArguments - } - return forwardedArguments + ["--no-binary-cache"] -} - private func warmBinaryCache(forwardedArguments: [String] = []) -> Int32 { guard !isCIEnvironment else { print("CI 환경이라 Tuist 바이너리 캐시 준비를 건너뜁니다.") @@ -188,8 +123,6 @@ private func warmBinaryCache(forwardedArguments: [String] = []) -> Int32 { print("--no-binary-cache 옵션이 있어 Tuist 바이너리 캐시 준비를 건너뜁니다.") return 0 } - let authStatus = prepareLocalTuistAccess(allowFailure: false) - guard authStatus == 0 else { return authStatus } return runTuist( arguments: cacheWarmArguments(forwardedArguments: forwardedArguments), environmentOverrides: ["TUIST_LOCAL_CACHE_ONLY": "true"] @@ -197,15 +130,14 @@ private func warmBinaryCache(forwardedArguments: [String] = []) -> Int32 { } private func installAndGenerate(forwardedArguments: [String] = []) -> Int32 { - let authStatus = prepareBinaryCacheIfNeeded( - forwardedArguments: forwardedArguments, - allowFailure: true - ) - guard authStatus == 0 else { return authStatus } let installStatus = runTuist(arguments: ["install"] + installArguments(forwardedArguments: forwardedArguments)) guard installStatus == 0 else { return installStatus } + let cacheStatus = warmBinaryCache( + forwardedArguments: forwardedArguments.filter { $0 == "--no-binary-cache" } + ) + guard cacheStatus == 0 else { return cacheStatus } let generateForwardedArguments = filteredGenerateArguments(forwardedArguments: forwardedArguments) - return runTuist(arguments: ["generate"] + generateArguments(forwardedArguments: generateForwardedArguments)) + return runTuist(arguments: ["generate"] + generateForwardedArguments) } private enum StepResult { @@ -375,17 +307,18 @@ private func renderGraph( return 1 } - let graph: String = if excludesDemo { + let graph: String + if excludesDemo { // 노드 선언(`AuthDemo [..]`)과 엣지(`AuthDemo -> Auth`) 양쪽에서 // 이름이 Demo 로 끝나는 줄을 지운다. dot 출력은 식별자에 따옴표를 붙이지 않는다. - rawGraph + graph = rawGraph .split(separator: "\n", omittingEmptySubsequences: false) .filter { line in line.range(of: #"\b[A-Za-z0-9_]+Demo\b"#, options: .regularExpression) == nil } .joined(separator: "\n") } else { - rawGraph + graph = rawGraph } let renderedGraphURL = workDirectory.appendingPathComponent("rendered-graph.dot") @@ -522,23 +455,17 @@ private func printHelp() { 🚀 Picke Tuist 도구 기본 명령어: - ./make setup # mise 설치 + Dashboard 확인 + install + 외부 캐시 준비 + generate - ./make generate # Demo 앱을 포함해 프로젝트 생성 - ./make build # 클린 + 의존성 설치 + 프로젝트 생성 - ./make install # 의존성 설치 + 프로젝트 생성 + ./make setup # mise 설치 + install + 로컬 외부 캐시 준비 + generate + ./make generate # 준비된 로컬 캐시를 사용해 프로젝트 생성 + ./make build # 클린 + 의존성 설치 + 로컬 외부 캐시 준비 + 프로젝트 생성 + ./make install # 의존성 설치 + 로컬 외부 캐시 준비 + 프로젝트 생성 ./make cache # 외부 바이너리 캐시 준비 - ./make cache:setup # 외부 바이너리 캐시 준비(cache 별칭) + ./make cache:setup # Xcode Compilation Cache 설정 ./make test # 전체 테스트 실행 ./make format # SwiftFormat 적용 ./make lint # SwiftFormat 검사 ./make clean # 프로젝트 정리 ./make reset # 앱 DerivedData 정리 + clean + install + generate - ./make edit # 매니페스트를 Xcode 로 열기 - - 점검: - ./make inspect # 프로젝트 구조 분석 - ./make inspect-imports # 암시적 의존성 검사 - ./make inspect-coverage # 코드 커버리지 분석 모듈 생성 (scaffold + 카탈로그 case + 엄브렐러 의존성 자동 등록): ./make feature <이름> [--case <케이스명>] @@ -571,18 +498,6 @@ private func execute(_ command: Command, forwardedArguments: [String]) -> Int32 return miseStatus } - if usesBinaryCache(forwardedArguments: forwardedArguments), !isCIEnvironment { - let authStatus = prepareLocalTuistAccess(allowFailure: false) - results.append(("Tuist Dashboard 인증/프로젝트 확인", authStatus == 0 ? .passed : .failed(authStatus))) - guard authStatus == 0 else { - printSetupSummary(results) - return authStatus - } - } else { - let reason = isCIEnvironment ? "CI 환경" : "--no-binary-cache" - results.append(("Tuist Dashboard 인증/프로젝트 확인", .skipped(reason))) - } - let installStatus = runTuist(arguments: ["install"] + installArguments(forwardedArguments: forwardedArguments)) results.append(("의존성 설치", installStatus == 0 ? .passed : .failed(installStatus))) guard installStatus == 0 else { @@ -604,19 +519,14 @@ private func execute(_ command: Command, forwardedArguments: [String]) -> Int32 let generateForwardedArguments = filteredGenerateArguments(forwardedArguments: forwardedArguments) let generateStatus = runTuist( - arguments: ["generate"] + generateArguments(forwardedArguments: generateForwardedArguments) + arguments: ["generate"] + generateForwardedArguments ) results.append(("프로젝트 생성", generateStatus == 0 ? .passed : .failed(generateStatus))) printSetupSummary(results) return generateStatus case .generate: - let authStatus = prepareBinaryCacheIfNeeded( - forwardedArguments: forwardedArguments, - allowFailure: true - ) - guard authStatus == 0 else { return authStatus } - return runTuist(arguments: ["generate"] + generateArguments(forwardedArguments: forwardedArguments)) + return runTuist(arguments: ["generate"] + forwardedArguments) case .build: let cleanStatus = runTuist(arguments: ["clean"]) @@ -630,15 +540,10 @@ private func execute(_ command: Command, forwardedArguments: [String]) -> Int32 return warmBinaryCache(forwardedArguments: forwardedArguments) case .cacheSetup: - return warmBinaryCache(forwardedArguments: forwardedArguments) + return runTuist(arguments: ["setup", "cache"] + forwardedArguments) case .test: - let authStatus = prepareBinaryCacheIfNeeded( - forwardedArguments: forwardedArguments, - allowFailure: true - ) - guard authStatus == 0 else { return authStatus } - return runTuist(arguments: ["test"] + generateArguments(forwardedArguments: forwardedArguments)) + return runTuist(arguments: ["test"] + forwardedArguments) case .format: return run("mise", arguments: ["exec", "--", "swiftformat", "."] + forwardedArguments) @@ -652,18 +557,6 @@ private func execute(_ command: Command, forwardedArguments: [String]) -> Int32 case .reset: return resetProject() - case .edit: - return runTuist(arguments: ["edit"] + forwardedArguments) - - case .inspect: - return runTuist(arguments: ["inspect"] + forwardedArguments) - - case .inspectImports: - return runTuist(arguments: ["inspect", "implicit-imports"] + forwardedArguments) - - case .inspectCoverage: - return runTuist(arguments: ["inspect", "code-coverage"] + forwardedArguments) - case .module, .moduleInit: return scaffoldModule(layer: nil, arguments: forwardedArguments) diff --git a/docs/diagrams/modules/API.svg b/docs/diagrams/modules/API.svg new file mode 100644 index 00000000..80a277d8 --- /dev/null +++ b/docs/diagrams/modules/API.svg @@ -0,0 +1,31 @@ + + + + + + +G + + + +API + +API + + + +PickeNetworkInterface + +PickeNetworkInterface + + + +API->PickeNetworkInterface + + + + + diff --git a/docs/diagrams/modules/APIEndpoint.svg b/docs/diagrams/modules/APIEndpoint.svg new file mode 100644 index 00000000..397d31c3 --- /dev/null +++ b/docs/diagrams/modules/APIEndpoint.svg @@ -0,0 +1,55 @@ + + + + + + +G + + + +API + +API + + + +APIEndpoint + +APIEndpoint + + + +APIEndpoint->API + + + + + +AuthDomainInterface + +AuthDomainInterface + + + +APIEndpoint->AuthDomainInterface + + + + + +PickeNetworkInterface + +PickeNetworkInterface + + + +APIEndpoint->PickeNetworkInterface + + + + + diff --git a/docs/diagrams/modules/Ad.svg b/docs/diagrams/modules/Ad.svg new file mode 100644 index 00000000..c464ed4a --- /dev/null +++ b/docs/diagrams/modules/Ad.svg @@ -0,0 +1,103 @@ + + + + + + +G + + + +Ad + +Ad + + + +AdDomainInterface + +AdDomainInterface + + + +Ad->AdDomainInterface + + + + + +AdInterface + +AdInterface + + + +Ad->AdInterface + + + + + +FeatureSharedUI + +FeatureSharedUI + + + +Ad->FeatureSharedUI + + + + + +PickeAnalyticsInterface + +PickeAnalyticsInterface + + + +Ad->PickeAnalyticsInterface + + + + + +PickeCoreLogger + +PickeCoreLogger + + + +Ad->PickeCoreLogger + + + + + +PickeDesignKit + +PickeDesignKit + + + +Ad->PickeDesignKit + + + + + +PickeSharedUI + +PickeSharedUI + + + +Ad->PickeSharedUI + + + + + diff --git a/docs/diagrams/modules/AdDomain.svg b/docs/diagrams/modules/AdDomain.svg new file mode 100644 index 00000000..ee99dee5 --- /dev/null +++ b/docs/diagrams/modules/AdDomain.svg @@ -0,0 +1,43 @@ + + + + + + +G + + + +AdDomain + +AdDomain + + + +AdDomainInterface + +AdDomainInterface + + + +AdDomain->AdDomainInterface + + + + + +ServiceAssembly + +ServiceAssembly + + + +AdDomain->ServiceAssembly + + + + + diff --git a/docs/diagrams/modules/AppUpdateDomain.svg b/docs/diagrams/modules/AppUpdateDomain.svg new file mode 100644 index 00000000..8f65d473 --- /dev/null +++ b/docs/diagrams/modules/AppUpdateDomain.svg @@ -0,0 +1,43 @@ + + + + + + +G + + + +AppUpdateDomain + +AppUpdateDomain + + + +AppUpdateDomainInterface + +AppUpdateDomainInterface + + + +AppUpdateDomain->AppUpdateDomainInterface + + + + + +ServiceAssembly + +ServiceAssembly + + + +AppUpdateDomain->ServiceAssembly + + + + + diff --git a/docs/diagrams/modules/AttendanceDomain.svg b/docs/diagrams/modules/AttendanceDomain.svg new file mode 100644 index 00000000..948f9410 --- /dev/null +++ b/docs/diagrams/modules/AttendanceDomain.svg @@ -0,0 +1,43 @@ + + + + + + +G + + + +AttendanceDomain + +AttendanceDomain + + + +AttendanceDomainInterface + +AttendanceDomainInterface + + + +AttendanceDomain->AttendanceDomainInterface + + + + + +ServiceAssembly + +ServiceAssembly + + + +AttendanceDomain->ServiceAssembly + + + + + diff --git a/docs/diagrams/modules/AudioPlayerService.svg b/docs/diagrams/modules/AudioPlayerService.svg new file mode 100644 index 00000000..c0642f26 --- /dev/null +++ b/docs/diagrams/modules/AudioPlayerService.svg @@ -0,0 +1,31 @@ + + + + + + +G + + + +AudioPlayerService + +AudioPlayerService + + + +AudioPlayerServiceInterface + +AudioPlayerServiceInterface + + + +AudioPlayerService->AudioPlayerServiceInterface + + + + + diff --git a/docs/diagrams/modules/Auth.svg b/docs/diagrams/modules/Auth.svg new file mode 100644 index 00000000..8c65756a --- /dev/null +++ b/docs/diagrams/modules/Auth.svg @@ -0,0 +1,91 @@ + + + + + + +G + + + +Auth + +Auth + + + +AuthDomainInterface + +AuthDomainInterface + + + +Auth->AuthDomainInterface + + + + + +AuthInterface + +AuthInterface + + + +Auth->AuthInterface + + + + + +PickeAnalyticsInterface + +PickeAnalyticsInterface + + + +Auth->PickeAnalyticsInterface + + + + + +PickeCoreLogger + +PickeCoreLogger + + + +Auth->PickeCoreLogger + + + + + +PickeDesignKit + +PickeDesignKit + + + +Auth->PickeDesignKit + + + + + +PickeSharedUI + +PickeSharedUI + + + +Auth->PickeSharedUI + + + + + diff --git a/docs/diagrams/modules/AuthDomain.svg b/docs/diagrams/modules/AuthDomain.svg new file mode 100644 index 00000000..83566181 --- /dev/null +++ b/docs/diagrams/modules/AuthDomain.svg @@ -0,0 +1,73 @@ + + + + + + +G + + + +AuthDomain + +AuthDomain + + + +AuthDomainInterface + +AuthDomainInterface + + + +AuthDomain->AuthDomainInterface + + + + + +PickeAuthInterface + +PickeAuthInterface + + + +AuthDomain->PickeAuthInterface + + + + + +PickeStorageInterface + +PickeStorageInterface + + + +AuthDomain->PickeStorageInterface + + + + + +ServiceAssembly + +ServiceAssembly + + + +AuthDomain->ServiceAssembly + + + + + +AuthDomainInterface->PickeStorageInterface + + + + + diff --git a/docs/diagrams/modules/Battle.svg b/docs/diagrams/modules/Battle.svg new file mode 100644 index 00000000..a5b53e57 --- /dev/null +++ b/docs/diagrams/modules/Battle.svg @@ -0,0 +1,103 @@ + + + + + + +G + + + +Battle + +Battle + + + +BattleDomainInterface + +BattleDomainInterface + + + +Battle->BattleDomainInterface + + + + + +BattleInterface + +BattleInterface + + + +Battle->BattleInterface + + + + + +PickeAnalyticsInterface + +PickeAnalyticsInterface + + + +Battle->PickeAnalyticsInterface + + + + + +PickeCoreLogger + +PickeCoreLogger + + + +Battle->PickeCoreLogger + + + + + +PickeCoreUtility + +PickeCoreUtility + + + +Battle->PickeCoreUtility + + + + + +PickeDesignKit + +PickeDesignKit + + + +Battle->PickeDesignKit + + + + + +PickeSharedUI + +PickeSharedUI + + + +Battle->PickeSharedUI + + + + + diff --git a/docs/diagrams/modules/BattleDomain.svg b/docs/diagrams/modules/BattleDomain.svg new file mode 100644 index 00000000..7683fa3f --- /dev/null +++ b/docs/diagrams/modules/BattleDomain.svg @@ -0,0 +1,61 @@ + + + + + + +G + + + +BattleDomain + +BattleDomain + + + +BattleDomainInterface + +BattleDomainInterface + + + +BattleDomain->BattleDomainInterface + + + + + +HomeDomainInterface + +HomeDomainInterface + + + +BattleDomain->HomeDomainInterface + + + + + +ServiceAssembly + +ServiceAssembly + + + +BattleDomain->ServiceAssembly + + + + + +BattleDomainInterface->HomeDomainInterface + + + + + diff --git a/docs/diagrams/modules/Chat.svg b/docs/diagrams/modules/Chat.svg new file mode 100644 index 00000000..fa29be3b --- /dev/null +++ b/docs/diagrams/modules/Chat.svg @@ -0,0 +1,175 @@ + + + + + + +G + + + +Ad + +Ad + + + +AudioPlayerServiceInterface + +AudioPlayerServiceInterface + + + +BattleDomainInterface + +BattleDomainInterface + + + +Chat + +Chat + + + +Chat->Ad + + + + + +Chat->AudioPlayerServiceInterface + + + + + +Chat->BattleDomainInterface + + + + + +ChatInterface + +ChatInterface + + + +Chat->ChatInterface + + + + + +CommentDomainInterface + +CommentDomainInterface + + + +Chat->CommentDomainInterface + + + + + +HomeDomainInterface + +HomeDomainInterface + + + +Chat->HomeDomainInterface + + + + + +PerspectiveDomainInterface + +PerspectiveDomainInterface + + + +Chat->PerspectiveDomainInterface + + + + + +PickeAnalyticsInterface + +PickeAnalyticsInterface + + + +Chat->PickeAnalyticsInterface + + + + + +PickeCoreLogger + +PickeCoreLogger + + + +Chat->PickeCoreLogger + + + + + +PickeCoreUtility + +PickeCoreUtility + + + +Chat->PickeCoreUtility + + + + + +PickeDesignKit + +PickeDesignKit + + + +Chat->PickeDesignKit + + + + + +PickeNetwork + +PickeNetwork + + + +Chat->PickeNetwork + + + + + +PickeSharedUI + +PickeSharedUI + + + +Chat->PickeSharedUI + + + + + diff --git a/docs/diagrams/modules/CommentDomain.svg b/docs/diagrams/modules/CommentDomain.svg new file mode 100644 index 00000000..f4dcd774 --- /dev/null +++ b/docs/diagrams/modules/CommentDomain.svg @@ -0,0 +1,61 @@ + + + + + + +G + + + +BattleDomainInterface + +BattleDomainInterface + + + +CommentDomain + +CommentDomain + + + +CommentDomain->BattleDomainInterface + + + + + +CommentDomainInterface + +CommentDomainInterface + + + +CommentDomain->CommentDomainInterface + + + + + +ServiceAssembly + +ServiceAssembly + + + +CommentDomain->ServiceAssembly + + + + + +CommentDomainInterface->BattleDomainInterface + + + + + diff --git a/docs/diagrams/modules/CoreAssembly.svg b/docs/diagrams/modules/CoreAssembly.svg new file mode 100644 index 00000000..a52bd109 --- /dev/null +++ b/docs/diagrams/modules/CoreAssembly.svg @@ -0,0 +1,91 @@ + + + + + + +G + + + +CoreAssembly + +CoreAssembly + + + +PickeCoreLogger + +PickeCoreLogger + + + +CoreAssembly->PickeCoreLogger + + + + + +PickeCoreUI + +PickeCoreUI + + + +CoreAssembly->PickeCoreUI + + + + + +PickeCoreUtility + +PickeCoreUtility + + + +CoreAssembly->PickeCoreUtility + + + + + +PickeNetwork + +PickeNetwork + + + +CoreAssembly->PickeNetwork + + + + + +PickeStorage + +PickeStorage + + + +CoreAssembly->PickeStorage + + + + + +PickeThirdParty + +PickeThirdParty + + + +CoreAssembly->PickeThirdParty + + + + + diff --git a/docs/diagrams/modules/DeviceService.svg b/docs/diagrams/modules/DeviceService.svg new file mode 100644 index 00000000..4f639a5e --- /dev/null +++ b/docs/diagrams/modules/DeviceService.svg @@ -0,0 +1,67 @@ + + + + + + +G + + + +APIEndpoint + +APIEndpoint + + + +DeviceService + +DeviceService + + + +DeviceService->APIEndpoint + + + + + +DeviceServiceInterface + +DeviceServiceInterface + + + +DeviceService->DeviceServiceInterface + + + + + +PickeCoreLogger + +PickeCoreLogger + + + +DeviceService->PickeCoreLogger + + + + + +PickeNetwork + +PickeNetwork + + + +DeviceService->PickeNetwork + + + + + diff --git a/docs/diagrams/modules/DomainAssembly.svg b/docs/diagrams/modules/DomainAssembly.svg new file mode 100644 index 00000000..4e2fcd38 --- /dev/null +++ b/docs/diagrams/modules/DomainAssembly.svg @@ -0,0 +1,151 @@ + + + + + + +G + + + +AdDomain + +AdDomain + + + +AppUpdateDomain + +AppUpdateDomain + + + +AttendanceDomain + +AttendanceDomain + + + +AuthDomain + +AuthDomain + + + +BattleDomain + +BattleDomain + + + +CommentDomain + +CommentDomain + + + +DomainAssembly + +DomainAssembly + + + +DomainAssembly->AdDomain + + + + + +DomainAssembly->AppUpdateDomain + + + + + +DomainAssembly->AttendanceDomain + + + + + +DomainAssembly->AuthDomain + + + + + +DomainAssembly->BattleDomain + + + + + +DomainAssembly->CommentDomain + + + + + +HomeDomain + +HomeDomain + + + +DomainAssembly->HomeDomain + + + + + +NotificationDomain + +NotificationDomain + + + +DomainAssembly->NotificationDomain + + + + + +PerspectiveDomain + +PerspectiveDomain + + + +DomainAssembly->PerspectiveDomain + + + + + +ProfileDomain + +ProfileDomain + + + +DomainAssembly->ProfileDomain + + + + + +SearchDomain + +SearchDomain + + + +DomainAssembly->SearchDomain + + + + + diff --git a/docs/diagrams/modules/FeatureAssembly.svg b/docs/diagrams/modules/FeatureAssembly.svg new file mode 100644 index 00000000..82d8ed74 --- /dev/null +++ b/docs/diagrams/modules/FeatureAssembly.svg @@ -0,0 +1,139 @@ + + + + + + +G + + + +Ad + +Ad + + + +Auth + +Auth + + + +Battle + +Battle + + + +Chat + +Chat + + + +FeatureAssembly + +FeatureAssembly + + + +FeatureAssembly->Ad + + + + + +FeatureAssembly->Auth + + + + + +FeatureAssembly->Battle + + + + + +FeatureAssembly->Chat + + + + + +FeatureSharedUI + +FeatureSharedUI + + + +FeatureAssembly->FeatureSharedUI + + + + + +Hifi + +Hifi + + + +FeatureAssembly->Hifi + + + + + +Home + +Home + + + +FeatureAssembly->Home + + + + + +Notification + +Notification + + + +FeatureAssembly->Notification + + + + + +Profile + +Profile + + + +FeatureAssembly->Profile + + + + + +Web + +Web + + + +FeatureAssembly->Web + + + + + diff --git a/docs/diagrams/modules/FeatureSharedUI.svg b/docs/diagrams/modules/FeatureSharedUI.svg new file mode 100644 index 00000000..42759b98 --- /dev/null +++ b/docs/diagrams/modules/FeatureSharedUI.svg @@ -0,0 +1,31 @@ + + + + + + +G + + + +FeatureSharedUI + +FeatureSharedUI + + + +PickeCoreLogger + +PickeCoreLogger + + + +FeatureSharedUI->PickeCoreLogger + + + + + diff --git a/docs/diagrams/modules/Hifi.svg b/docs/diagrams/modules/Hifi.svg new file mode 100644 index 00000000..e7619dae --- /dev/null +++ b/docs/diagrams/modules/Hifi.svg @@ -0,0 +1,175 @@ + + + + + + +G + + + +Ad + +Ad + + + +AdDomainInterface + +AdDomainInterface + + + +BattleDomainInterface + +BattleDomainInterface + + + +FeatureSharedUI + +FeatureSharedUI + + + +Hifi + +Hifi + + + +Hifi->Ad + + + + + +Hifi->AdDomainInterface + + + + + +Hifi->BattleDomainInterface + + + + + +Hifi->FeatureSharedUI + + + + + +HifiInterface + +HifiInterface + + + +Hifi->HifiInterface + + + + + +HomeDomainInterface + +HomeDomainInterface + + + +Hifi->HomeDomainInterface + + + + + +NotificationDomainInterface + +NotificationDomainInterface + + + +Hifi->NotificationDomainInterface + + + + + +PickeAnalyticsInterface + +PickeAnalyticsInterface + + + +Hifi->PickeAnalyticsInterface + + + + + +PickeCoreLogger + +PickeCoreLogger + + + +Hifi->PickeCoreLogger + + + + + +PickeCoreUtility + +PickeCoreUtility + + + +Hifi->PickeCoreUtility + + + + + +PickeDesignKit + +PickeDesignKit + + + +Hifi->PickeDesignKit + + + + + +PickeSharedUI + +PickeSharedUI + + + +Hifi->PickeSharedUI + + + + + +SearchDomainInterface + +SearchDomainInterface + + + +Hifi->SearchDomainInterface + + + + + diff --git a/docs/diagrams/modules/Home.svg b/docs/diagrams/modules/Home.svg new file mode 100644 index 00000000..78d75302 --- /dev/null +++ b/docs/diagrams/modules/Home.svg @@ -0,0 +1,151 @@ + + + + + + +G + + + +AttendanceDomainInterface + +AttendanceDomainInterface + + + +AuthDomainInterface + +AuthDomainInterface + + + +BattleDomainInterface + +BattleDomainInterface + + + +FeatureSharedUI + +FeatureSharedUI + + + +Home + +Home + + + +Home->AttendanceDomainInterface + + + + + +Home->AuthDomainInterface + + + + + +Home->BattleDomainInterface + + + + + +Home->FeatureSharedUI + + + + + +HomeDomainInterface + +HomeDomainInterface + + + +Home->HomeDomainInterface + + + + + +HomeInterface + +HomeInterface + + + +Home->HomeInterface + + + + + +NotificationDomainInterface + +NotificationDomainInterface + + + +Home->NotificationDomainInterface + + + + + +PickeAnalyticsInterface + +PickeAnalyticsInterface + + + +Home->PickeAnalyticsInterface + + + + + +PickeCoreLogger + +PickeCoreLogger + + + +Home->PickeCoreLogger + + + + + +PickeDesignKit + +PickeDesignKit + + + +Home->PickeDesignKit + + + + + +PickeSharedUI + +PickeSharedUI + + + +Home->PickeSharedUI + + + + + diff --git a/docs/diagrams/modules/HomeDomain.svg b/docs/diagrams/modules/HomeDomain.svg new file mode 100644 index 00000000..d08d61f1 --- /dev/null +++ b/docs/diagrams/modules/HomeDomain.svg @@ -0,0 +1,55 @@ + + + + + + +G + + + +AuthDomainInterface + +AuthDomainInterface + + + +HomeDomain + +HomeDomain + + + +HomeDomain->AuthDomainInterface + + + + + +HomeDomainInterface + +HomeDomainInterface + + + +HomeDomain->HomeDomainInterface + + + + + +ServiceAssembly + +ServiceAssembly + + + +HomeDomain->ServiceAssembly + + + + + diff --git a/docs/diagrams/modules/Notification.svg b/docs/diagrams/modules/Notification.svg new file mode 100644 index 00000000..69be83ed --- /dev/null +++ b/docs/diagrams/modules/Notification.svg @@ -0,0 +1,103 @@ + + + + + + +G + + + +Notification + +Notification + + + +NotificationDomainInterface + +NotificationDomainInterface + + + +Notification->NotificationDomainInterface + + + + + +NotificationInterface + +NotificationInterface + + + +Notification->NotificationInterface + + + + + +PickeAnalyticsInterface + +PickeAnalyticsInterface + + + +Notification->PickeAnalyticsInterface + + + + + +PickeCoreLogger + +PickeCoreLogger + + + +Notification->PickeCoreLogger + + + + + +PickeCoreUtility + +PickeCoreUtility + + + +Notification->PickeCoreUtility + + + + + +PickeDesignKit + +PickeDesignKit + + + +Notification->PickeDesignKit + + + + + +PickeSharedUI + +PickeSharedUI + + + +Notification->PickeSharedUI + + + + + diff --git a/docs/diagrams/modules/NotificationDomain.svg b/docs/diagrams/modules/NotificationDomain.svg new file mode 100644 index 00000000..0ab6c907 --- /dev/null +++ b/docs/diagrams/modules/NotificationDomain.svg @@ -0,0 +1,43 @@ + + + + + + +G + + + +NotificationDomain + +NotificationDomain + + + +NotificationDomainInterface + +NotificationDomainInterface + + + +NotificationDomain->NotificationDomainInterface + + + + + +ServiceAssembly + +ServiceAssembly + + + +NotificationDomain->ServiceAssembly + + + + + diff --git a/docs/diagrams/modules/PerspectiveDomain.svg b/docs/diagrams/modules/PerspectiveDomain.svg new file mode 100644 index 00000000..0f19b199 --- /dev/null +++ b/docs/diagrams/modules/PerspectiveDomain.svg @@ -0,0 +1,79 @@ + + + + + + +G + + + +BattleDomainInterface + +BattleDomainInterface + + + +CommentDomainInterface + +CommentDomainInterface + + + +PerspectiveDomain + +PerspectiveDomain + + + +PerspectiveDomain->BattleDomainInterface + + + + + +PerspectiveDomain->CommentDomainInterface + + + + + +PerspectiveDomainInterface + +PerspectiveDomainInterface + + + +PerspectiveDomain->PerspectiveDomainInterface + + + + + +ServiceAssembly + +ServiceAssembly + + + +PerspectiveDomain->ServiceAssembly + + + + + +PerspectiveDomainInterface->BattleDomainInterface + + + + + +PerspectiveDomainInterface->CommentDomainInterface + + + + + diff --git a/docs/diagrams/modules/Picke.svg b/docs/diagrams/modules/Picke.svg new file mode 100644 index 00000000..973f98b3 --- /dev/null +++ b/docs/diagrams/modules/Picke.svg @@ -0,0 +1,103 @@ + + + + + + +G + + + +DomainAssembly + +DomainAssembly + + + +FeatureAssembly + +FeatureAssembly + + + +NotificationInterface + +NotificationInterface + + + +Picke + +Picke + + + +Picke->DomainAssembly + + + + + +Picke->FeatureAssembly + + + + + +Picke->NotificationInterface + + + + + +PickeAnimation + +PickeAnimation + + + +Picke->PickeAnimation + + + + + +PickeConfig + +PickeConfig + + + +Picke->PickeConfig + + + + + +PickeStorageInterface + +PickeStorageInterface + + + +Picke->PickeStorageInterface + + + + + +ServiceAssembly + +ServiceAssembly + + + +Picke->ServiceAssembly + + + + + diff --git a/docs/diagrams/modules/PickeAnalytics.svg b/docs/diagrams/modules/PickeAnalytics.svg new file mode 100644 index 00000000..2fd03bf8 --- /dev/null +++ b/docs/diagrams/modules/PickeAnalytics.svg @@ -0,0 +1,55 @@ + + + + + + +G + + + +PickeAnalytics + +PickeAnalytics + + + +PickeAnalyticsInterface + +PickeAnalyticsInterface + + + +PickeAnalytics->PickeAnalyticsInterface + + + + + +PickeCoreLogger + +PickeCoreLogger + + + +PickeAnalytics->PickeCoreLogger + + + + + +PickeNetwork + +PickeNetwork + + + +PickeAnalytics->PickeNetwork + + + + + diff --git a/docs/diagrams/modules/PickeAnimation.svg b/docs/diagrams/modules/PickeAnimation.svg new file mode 100644 index 00000000..c31771d2 --- /dev/null +++ b/docs/diagrams/modules/PickeAnimation.svg @@ -0,0 +1,19 @@ + + + + + + +G + + + +PickeAnimation + +PickeAnimation + + + diff --git a/docs/diagrams/modules/PickeAuth.svg b/docs/diagrams/modules/PickeAuth.svg new file mode 100644 index 00000000..bb62e6fc --- /dev/null +++ b/docs/diagrams/modules/PickeAuth.svg @@ -0,0 +1,103 @@ + + + + + + +G + + + +APIEndpoint + +APIEndpoint + + + +PickeAuth + +PickeAuth + + + +PickeAuth->APIEndpoint + + + + + +PickeAuthInterface + +PickeAuthInterface + + + +PickeAuth->PickeAuthInterface + + + + + +PickeCoreLogger + +PickeCoreLogger + + + +PickeAuth->PickeCoreLogger + + + + + +PickeNetwork + +PickeNetwork + + + +PickeAuth->PickeNetwork + + + + + +PickeStorage + +PickeStorage + + + +PickeAuth->PickeStorage + + + + + +PickeStorageInterface + +PickeStorageInterface + + + +PickeAuth->PickeStorageInterface + + + + + +PickeNetworkInterface + +PickeNetworkInterface + + + +PickeAuthInterface->PickeNetworkInterface + + + + + diff --git a/docs/diagrams/modules/PickeConfig.svg b/docs/diagrams/modules/PickeConfig.svg new file mode 100644 index 00000000..74868169 --- /dev/null +++ b/docs/diagrams/modules/PickeConfig.svg @@ -0,0 +1,19 @@ + + + + + + +G + + + +PickeConfig + +PickeConfig + + + diff --git a/docs/diagrams/modules/PickeCoreLogger.svg b/docs/diagrams/modules/PickeCoreLogger.svg new file mode 100644 index 00000000..580a54b5 --- /dev/null +++ b/docs/diagrams/modules/PickeCoreLogger.svg @@ -0,0 +1,19 @@ + + + + + + +G + + + +PickeCoreLogger + +PickeCoreLogger + + + diff --git a/docs/diagrams/modules/PickeCoreUI.svg b/docs/diagrams/modules/PickeCoreUI.svg new file mode 100644 index 00000000..807256ba --- /dev/null +++ b/docs/diagrams/modules/PickeCoreUI.svg @@ -0,0 +1,19 @@ + + + + + + +G + + + +PickeCoreUI + +PickeCoreUI + + + diff --git a/docs/diagrams/modules/PickeCoreUtility.svg b/docs/diagrams/modules/PickeCoreUtility.svg new file mode 100644 index 00000000..1db7edfc --- /dev/null +++ b/docs/diagrams/modules/PickeCoreUtility.svg @@ -0,0 +1,31 @@ + + + + + + +G + + + +PickeCoreUtility + +PickeCoreUtility + + + +PickeNetwork + +PickeNetwork + + + +PickeCoreUtility->PickeNetwork + + + + + diff --git a/docs/diagrams/modules/PickeDesignKit.svg b/docs/diagrams/modules/PickeDesignKit.svg new file mode 100644 index 00000000..5d5af375 --- /dev/null +++ b/docs/diagrams/modules/PickeDesignKit.svg @@ -0,0 +1,31 @@ + + + + + + +G + + + +PickeCoreUI + +PickeCoreUI + + + +PickeDesignKit + +PickeDesignKit + + + +PickeDesignKit->PickeCoreUI + + + + + diff --git a/docs/diagrams/modules/PickeNetwork.svg b/docs/diagrams/modules/PickeNetwork.svg new file mode 100644 index 00000000..09d187b8 --- /dev/null +++ b/docs/diagrams/modules/PickeNetwork.svg @@ -0,0 +1,43 @@ + + + + + + +G + + + +PickeCoreLogger + +PickeCoreLogger + + + +PickeNetwork + +PickeNetwork + + + +PickeNetwork->PickeCoreLogger + + + + + +PickeNetworkInterface + +PickeNetworkInterface + + + +PickeNetwork->PickeNetworkInterface + + + + + diff --git a/docs/diagrams/modules/PickeSharedUI.svg b/docs/diagrams/modules/PickeSharedUI.svg new file mode 100644 index 00000000..aee3fbe9 --- /dev/null +++ b/docs/diagrams/modules/PickeSharedUI.svg @@ -0,0 +1,31 @@ + + + + + + +G + + + +PickeDesignKit + +PickeDesignKit + + + +PickeSharedUI + +PickeSharedUI + + + +PickeSharedUI->PickeDesignKit + + + + + diff --git a/docs/diagrams/modules/PickeStorage.svg b/docs/diagrams/modules/PickeStorage.svg new file mode 100644 index 00000000..8d071e2d --- /dev/null +++ b/docs/diagrams/modules/PickeStorage.svg @@ -0,0 +1,43 @@ + + + + + + +G + + + +PickeCoreLogger + +PickeCoreLogger + + + +PickeStorage + +PickeStorage + + + +PickeStorage->PickeCoreLogger + + + + + +PickeStorageInterface + +PickeStorageInterface + + + +PickeStorage->PickeStorageInterface + + + + + diff --git a/docs/diagrams/modules/PickeThirdParty.svg b/docs/diagrams/modules/PickeThirdParty.svg new file mode 100644 index 00000000..7f58b5c7 --- /dev/null +++ b/docs/diagrams/modules/PickeThirdParty.svg @@ -0,0 +1,31 @@ + + + + + + +G + + + +PickeCoreUtility + +PickeCoreUtility + + + +PickeThirdParty + +PickeThirdParty + + + +PickeThirdParty->PickeCoreUtility + + + + + diff --git a/docs/diagrams/modules/Profile.svg b/docs/diagrams/modules/Profile.svg new file mode 100644 index 00000000..7c6563cf --- /dev/null +++ b/docs/diagrams/modules/Profile.svg @@ -0,0 +1,205 @@ + + + + + + +G + + + +Ad + +Ad + + + +AdInterface + +AdInterface + + + +AuthDomainInterface + +AuthDomainInterface + + + +BattleDomainInterface + +BattleDomainInterface + + + +DeviceServiceInterface + +DeviceServiceInterface + + + +NotificationDomainInterface + +NotificationDomainInterface + + + +PickeAnalyticsInterface + +PickeAnalyticsInterface + + + +PickeAuthInterface + +PickeAuthInterface + + + +PickeCoreLogger + +PickeCoreLogger + + + +PickeCoreUtility + +PickeCoreUtility + + + +PickeDesignKit + +PickeDesignKit + + + +PickeSharedUI + +PickeSharedUI + + + +PickeStorageInterface + +PickeStorageInterface + + + +Profile + +Profile + + + +Profile->Ad + + + + + +Profile->AdInterface + + + + + +Profile->AuthDomainInterface + + + + + +Profile->BattleDomainInterface + + + + + +Profile->DeviceServiceInterface + + + + + +Profile->NotificationDomainInterface + + + + + +Profile->PickeAnalyticsInterface + + + + + +Profile->PickeAuthInterface + + + + + +Profile->PickeCoreLogger + + + + + +Profile->PickeCoreUtility + + + + + +Profile->PickeDesignKit + + + + + +Profile->PickeSharedUI + + + + + +Profile->PickeStorageInterface + + + + + +ProfileDomainInterface + +ProfileDomainInterface + + + +Profile->ProfileDomainInterface + + + + + +ProfileInterface + +ProfileInterface + + + +Profile->ProfileInterface + + + + + +ProfileInterface->ProfileDomainInterface + + + + + diff --git a/docs/diagrams/modules/ProfileDomain.svg b/docs/diagrams/modules/ProfileDomain.svg new file mode 100644 index 00000000..d9ae536f --- /dev/null +++ b/docs/diagrams/modules/ProfileDomain.svg @@ -0,0 +1,43 @@ + + + + + + +G + + + +ProfileDomain + +ProfileDomain + + + +ProfileDomainInterface + +ProfileDomainInterface + + + +ProfileDomain->ProfileDomainInterface + + + + + +ServiceAssembly + +ServiceAssembly + + + +ProfileDomain->ServiceAssembly + + + + + diff --git a/docs/diagrams/modules/SearchDomain.svg b/docs/diagrams/modules/SearchDomain.svg new file mode 100644 index 00000000..49ac5fa8 --- /dev/null +++ b/docs/diagrams/modules/SearchDomain.svg @@ -0,0 +1,73 @@ + + + + + + +G + + + +BattleDomainInterface + +BattleDomainInterface + + + +HomeDomainInterface + +HomeDomainInterface + + + +SearchDomain + +SearchDomain + + + +SearchDomain->BattleDomainInterface + + + + + +SearchDomain->HomeDomainInterface + + + + + +SearchDomainInterface + +SearchDomainInterface + + + +SearchDomain->SearchDomainInterface + + + + + +ServiceAssembly + +ServiceAssembly + + + +SearchDomain->ServiceAssembly + + + + + +SearchDomainInterface->HomeDomainInterface + + + + + diff --git a/docs/diagrams/modules/ServiceAssembly.svg b/docs/diagrams/modules/ServiceAssembly.svg new file mode 100644 index 00000000..df866441 --- /dev/null +++ b/docs/diagrams/modules/ServiceAssembly.svg @@ -0,0 +1,127 @@ + + + + + + +G + + + +API + +API + + + +APIEndpoint + +APIEndpoint + + + +AudioPlayerService + +AudioPlayerService + + + +CoreAssembly + +CoreAssembly + + + +DeviceService + +DeviceService + + + +PickeAnalytics + +PickeAnalytics + + + +PickeAuth + +PickeAuth + + + +PickeAuthInterface + +PickeAuthInterface + + + +PickeConfig + +PickeConfig + + + +ServiceAssembly + +ServiceAssembly + + + +ServiceAssembly->API + + + + + +ServiceAssembly->APIEndpoint + + + + + +ServiceAssembly->AudioPlayerService + + + + + +ServiceAssembly->CoreAssembly + + + + + +ServiceAssembly->DeviceService + + + + + +ServiceAssembly->PickeAnalytics + + + + + +ServiceAssembly->PickeAuth + + + + + +ServiceAssembly->PickeAuthInterface + + + + + +ServiceAssembly->PickeConfig + + + + + diff --git a/docs/diagrams/modules/Web.svg b/docs/diagrams/modules/Web.svg new file mode 100644 index 00000000..3dcecb3d --- /dev/null +++ b/docs/diagrams/modules/Web.svg @@ -0,0 +1,43 @@ + + + + + + +G + + + +PickeDesignKit + +PickeDesignKit + + + +Web + +Web + + + +Web->PickeDesignKit + + + + + +WebInterface + +WebInterface + + + +Web->WebInterface + + + + + diff --git a/docs/diagrams/modules/manifest.json b/docs/diagrams/modules/manifest.json new file mode 100644 index 00000000..5562d2ef --- /dev/null +++ b/docs/diagrams/modules/manifest.json @@ -0,0 +1,1229 @@ +[ + { + "name": "Picke", + "layer": "App", + "path": "Projects/App/Project.swift", + "hasInterface": false, + "edges": [ + [ + "Picke", + "DomainAssembly" + ], + [ + "Picke", + "FeatureAssembly" + ], + [ + "Picke", + "NotificationInterface" + ], + [ + "Picke", + "PickeAnimation" + ], + [ + "Picke", + "PickeConfig" + ], + [ + "Picke", + "PickeStorageInterface" + ], + [ + "Picke", + "ServiceAssembly" + ] + ], + "external": [ + "googleMobileAds", + "kingfisher" + ] + }, + { + "name": "CoreAssembly", + "layer": "Core", + "path": "Projects/Core/CoreAssembly/Project.swift", + "hasInterface": false, + "edges": [ + [ + "CoreAssembly", + "PickeCoreLogger" + ], + [ + "CoreAssembly", + "PickeCoreUI" + ], + [ + "CoreAssembly", + "PickeCoreUtility" + ], + [ + "CoreAssembly", + "PickeNetwork" + ], + [ + "CoreAssembly", + "PickeStorage" + ], + [ + "CoreAssembly", + "PickeThirdParty" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "PickeCoreLogger", + "layer": "Core", + "path": "Projects/Core/PickeCoreLogger/Project.swift", + "hasInterface": false, + "edges": [], + "external": [] + }, + { + "name": "PickeCoreUI", + "layer": "Core", + "path": "Projects/Core/PickeCoreUI/Project.swift", + "hasInterface": false, + "edges": [], + "external": [] + }, + { + "name": "PickeCoreUtility", + "layer": "Core", + "path": "Projects/Core/PickeCoreUtility/Project.swift", + "hasInterface": false, + "edges": [ + [ + "PickeCoreUtility", + "PickeNetwork" + ] + ], + "external": [ + "dependencies" + ] + }, + { + "name": "PickeNetwork", + "layer": "Core", + "path": "Projects/Core/PickeNetwork/Project.swift", + "hasInterface": true, + "edges": [ + [ + "PickeNetwork", + "PickeCoreLogger" + ], + [ + "PickeNetwork", + "PickeNetworkInterface" + ] + ], + "external": [ + "alamofire", + "dependencies" + ] + }, + { + "name": "PickeStorage", + "layer": "Core", + "path": "Projects/Core/PickeStorage/Project.swift", + "hasInterface": true, + "edges": [ + [ + "PickeStorage", + "PickeCoreLogger" + ], + [ + "PickeStorage", + "PickeStorageInterface" + ] + ], + "external": [ + "composableArchitecture", + "sharing", + "sqliteData" + ] + }, + { + "name": "PickeThirdParty", + "layer": "Core", + "path": "Projects/Core/PickeThirdParty/Project.swift", + "hasInterface": false, + "edges": [ + [ + "PickeThirdParty", + "PickeCoreUtility" + ] + ], + "external": [ + "composableArchitecture", + "sdwebImage", + "tcaFlow" + ] + }, + { + "name": "AdDomain", + "layer": "Domain", + "path": "Projects/Domain/AdDomain/Project.swift", + "hasInterface": true, + "edges": [ + [ + "AdDomain", + "AdDomainInterface" + ], + [ + "AdDomain", + "ServiceAssembly" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "AppUpdateDomain", + "layer": "Domain", + "path": "Projects/Domain/AppUpdateDomain/Project.swift", + "hasInterface": true, + "edges": [ + [ + "AppUpdateDomain", + "AppUpdateDomainInterface" + ], + [ + "AppUpdateDomain", + "ServiceAssembly" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "AttendanceDomain", + "layer": "Domain", + "path": "Projects/Domain/AttendanceDomain/Project.swift", + "hasInterface": true, + "edges": [ + [ + "AttendanceDomain", + "AttendanceDomainInterface" + ], + [ + "AttendanceDomain", + "ServiceAssembly" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "AuthDomain", + "layer": "Domain", + "path": "Projects/Domain/AuthDomain/Project.swift", + "hasInterface": true, + "edges": [ + [ + "AuthDomain", + "AuthDomainInterface" + ], + [ + "AuthDomain", + "PickeAuthInterface" + ], + [ + "AuthDomain", + "PickeStorageInterface" + ], + [ + "AuthDomain", + "ServiceAssembly" + ], + [ + "AuthDomainInterface", + "PickeStorageInterface" + ] + ], + "external": [ + "composableArchitecture", + "googleSignIn", + "sharing" + ] + }, + { + "name": "BattleDomain", + "layer": "Domain", + "path": "Projects/Domain/BattleDomain/Project.swift", + "hasInterface": true, + "edges": [ + [ + "BattleDomain", + "BattleDomainInterface" + ], + [ + "BattleDomain", + "HomeDomainInterface" + ], + [ + "BattleDomain", + "ServiceAssembly" + ], + [ + "BattleDomainInterface", + "HomeDomainInterface" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "CommentDomain", + "layer": "Domain", + "path": "Projects/Domain/CommentDomain/Project.swift", + "hasInterface": true, + "edges": [ + [ + "CommentDomain", + "BattleDomainInterface" + ], + [ + "CommentDomain", + "CommentDomainInterface" + ], + [ + "CommentDomain", + "ServiceAssembly" + ], + [ + "CommentDomainInterface", + "BattleDomainInterface" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "DomainAssembly", + "layer": "Domain", + "path": "Projects/Domain/DomainAssembly/Project.swift", + "hasInterface": false, + "edges": [ + [ + "DomainAssembly", + "AdDomain" + ], + [ + "DomainAssembly", + "AppUpdateDomain" + ], + [ + "DomainAssembly", + "AttendanceDomain" + ], + [ + "DomainAssembly", + "AuthDomain" + ], + [ + "DomainAssembly", + "BattleDomain" + ], + [ + "DomainAssembly", + "CommentDomain" + ], + [ + "DomainAssembly", + "HomeDomain" + ], + [ + "DomainAssembly", + "NotificationDomain" + ], + [ + "DomainAssembly", + "PerspectiveDomain" + ], + [ + "DomainAssembly", + "ProfileDomain" + ], + [ + "DomainAssembly", + "SearchDomain" + ] + ], + "external": [] + }, + { + "name": "HomeDomain", + "layer": "Domain", + "path": "Projects/Domain/HomeDomain/Project.swift", + "hasInterface": true, + "edges": [ + [ + "HomeDomain", + "AuthDomainInterface" + ], + [ + "HomeDomain", + "HomeDomainInterface" + ], + [ + "HomeDomain", + "ServiceAssembly" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "NotificationDomain", + "layer": "Domain", + "path": "Projects/Domain/NotificationDomain/Project.swift", + "hasInterface": true, + "edges": [ + [ + "NotificationDomain", + "NotificationDomainInterface" + ], + [ + "NotificationDomain", + "ServiceAssembly" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "PerspectiveDomain", + "layer": "Domain", + "path": "Projects/Domain/PerspectiveDomain/Project.swift", + "hasInterface": true, + "edges": [ + [ + "PerspectiveDomain", + "BattleDomainInterface" + ], + [ + "PerspectiveDomain", + "CommentDomainInterface" + ], + [ + "PerspectiveDomain", + "PerspectiveDomainInterface" + ], + [ + "PerspectiveDomain", + "ServiceAssembly" + ], + [ + "PerspectiveDomainInterface", + "BattleDomainInterface" + ], + [ + "PerspectiveDomainInterface", + "CommentDomainInterface" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "ProfileDomain", + "layer": "Domain", + "path": "Projects/Domain/ProfileDomain/Project.swift", + "hasInterface": true, + "edges": [ + [ + "ProfileDomain", + "ProfileDomainInterface" + ], + [ + "ProfileDomain", + "ServiceAssembly" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "SearchDomain", + "layer": "Domain", + "path": "Projects/Domain/SearchDomain/Project.swift", + "hasInterface": true, + "edges": [ + [ + "SearchDomain", + "BattleDomainInterface" + ], + [ + "SearchDomain", + "HomeDomainInterface" + ], + [ + "SearchDomain", + "SearchDomainInterface" + ], + [ + "SearchDomain", + "ServiceAssembly" + ], + [ + "SearchDomainInterface", + "HomeDomainInterface" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "Ad", + "layer": "Feature", + "path": "Projects/Feature/Ad/Project.swift", + "hasInterface": true, + "edges": [ + [ + "Ad", + "AdDomainInterface" + ], + [ + "Ad", + "AdInterface" + ], + [ + "Ad", + "FeatureSharedUI" + ], + [ + "Ad", + "PickeAnalyticsInterface" + ], + [ + "Ad", + "PickeCoreLogger" + ], + [ + "Ad", + "PickeDesignKit" + ], + [ + "Ad", + "PickeSharedUI" + ] + ], + "external": [ + "adFit", + "composableArchitecture", + "googleMobileAds" + ] + }, + { + "name": "Auth", + "layer": "Feature", + "path": "Projects/Feature/Auth/Project.swift", + "hasInterface": true, + "edges": [ + [ + "Auth", + "AuthDomainInterface" + ], + [ + "Auth", + "AuthInterface" + ], + [ + "Auth", + "PickeAnalyticsInterface" + ], + [ + "Auth", + "PickeCoreLogger" + ], + [ + "Auth", + "PickeDesignKit" + ], + [ + "Auth", + "PickeSharedUI" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "Battle", + "layer": "Feature", + "path": "Projects/Feature/Battle/Project.swift", + "hasInterface": true, + "edges": [ + [ + "Battle", + "BattleDomainInterface" + ], + [ + "Battle", + "BattleInterface" + ], + [ + "Battle", + "PickeAnalyticsInterface" + ], + [ + "Battle", + "PickeCoreLogger" + ], + [ + "Battle", + "PickeCoreUtility" + ], + [ + "Battle", + "PickeDesignKit" + ], + [ + "Battle", + "PickeSharedUI" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "Chat", + "layer": "Feature", + "path": "Projects/Feature/Chat/Project.swift", + "hasInterface": true, + "edges": [ + [ + "Chat", + "Ad" + ], + [ + "Chat", + "AudioPlayerServiceInterface" + ], + [ + "Chat", + "BattleDomainInterface" + ], + [ + "Chat", + "ChatInterface" + ], + [ + "Chat", + "CommentDomainInterface" + ], + [ + "Chat", + "HomeDomainInterface" + ], + [ + "Chat", + "PerspectiveDomainInterface" + ], + [ + "Chat", + "PickeAnalyticsInterface" + ], + [ + "Chat", + "PickeCoreLogger" + ], + [ + "Chat", + "PickeCoreUtility" + ], + [ + "Chat", + "PickeDesignKit" + ], + [ + "Chat", + "PickeNetwork" + ], + [ + "Chat", + "PickeSharedUI" + ] + ], + "external": [ + "composableArchitecture", + "tcaFlow" + ] + }, + { + "name": "FeatureAssembly", + "layer": "Feature", + "path": "Projects/Feature/FeatureAssembly/Project.swift", + "hasInterface": false, + "edges": [ + [ + "FeatureAssembly", + "Ad" + ], + [ + "FeatureAssembly", + "Auth" + ], + [ + "FeatureAssembly", + "Battle" + ], + [ + "FeatureAssembly", + "Chat" + ], + [ + "FeatureAssembly", + "FeatureSharedUI" + ], + [ + "FeatureAssembly", + "Hifi" + ], + [ + "FeatureAssembly", + "Home" + ], + [ + "FeatureAssembly", + "Notification" + ], + [ + "FeatureAssembly", + "Profile" + ], + [ + "FeatureAssembly", + "Web" + ] + ], + "external": [] + }, + { + "name": "FeatureSharedUI", + "layer": "Feature", + "path": "Projects/Feature/FeatureSharedUI/Project.swift", + "hasInterface": false, + "edges": [ + [ + "FeatureSharedUI", + "PickeCoreLogger" + ] + ], + "external": [ + "adFit" + ] + }, + { + "name": "Hifi", + "layer": "Feature", + "path": "Projects/Feature/Hifi/Project.swift", + "hasInterface": true, + "edges": [ + [ + "Hifi", + "Ad" + ], + [ + "Hifi", + "AdDomainInterface" + ], + [ + "Hifi", + "BattleDomainInterface" + ], + [ + "Hifi", + "FeatureSharedUI" + ], + [ + "Hifi", + "HifiInterface" + ], + [ + "Hifi", + "HomeDomainInterface" + ], + [ + "Hifi", + "NotificationDomainInterface" + ], + [ + "Hifi", + "PickeAnalyticsInterface" + ], + [ + "Hifi", + "PickeCoreLogger" + ], + [ + "Hifi", + "PickeCoreUtility" + ], + [ + "Hifi", + "PickeDesignKit" + ], + [ + "Hifi", + "PickeSharedUI" + ], + [ + "Hifi", + "SearchDomainInterface" + ] + ], + "external": [ + "composableArchitecture", + "kingfisher" + ] + }, + { + "name": "Home", + "layer": "Feature", + "path": "Projects/Feature/Home/Project.swift", + "hasInterface": true, + "edges": [ + [ + "Home", + "AttendanceDomainInterface" + ], + [ + "Home", + "AuthDomainInterface" + ], + [ + "Home", + "BattleDomainInterface" + ], + [ + "Home", + "FeatureSharedUI" + ], + [ + "Home", + "HomeDomainInterface" + ], + [ + "Home", + "HomeInterface" + ], + [ + "Home", + "NotificationDomainInterface" + ], + [ + "Home", + "PickeAnalyticsInterface" + ], + [ + "Home", + "PickeCoreLogger" + ], + [ + "Home", + "PickeDesignKit" + ], + [ + "Home", + "PickeSharedUI" + ] + ], + "external": [ + "composableArchitecture", + "kingfisher", + "tcaFlow" + ] + }, + { + "name": "Notification", + "layer": "Feature", + "path": "Projects/Feature/Notification/Project.swift", + "hasInterface": true, + "edges": [ + [ + "Notification", + "NotificationDomainInterface" + ], + [ + "Notification", + "NotificationInterface" + ], + [ + "Notification", + "PickeAnalyticsInterface" + ], + [ + "Notification", + "PickeCoreLogger" + ], + [ + "Notification", + "PickeCoreUtility" + ], + [ + "Notification", + "PickeDesignKit" + ], + [ + "Notification", + "PickeSharedUI" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "Profile", + "layer": "Feature", + "path": "Projects/Feature/Profile/Project.swift", + "hasInterface": true, + "edges": [ + [ + "Profile", + "Ad" + ], + [ + "Profile", + "AdInterface" + ], + [ + "Profile", + "AuthDomainInterface" + ], + [ + "Profile", + "BattleDomainInterface" + ], + [ + "Profile", + "DeviceServiceInterface" + ], + [ + "Profile", + "NotificationDomainInterface" + ], + [ + "Profile", + "PickeAnalyticsInterface" + ], + [ + "Profile", + "PickeAuthInterface" + ], + [ + "Profile", + "PickeCoreLogger" + ], + [ + "Profile", + "PickeCoreUtility" + ], + [ + "Profile", + "PickeDesignKit" + ], + [ + "Profile", + "PickeSharedUI" + ], + [ + "Profile", + "PickeStorageInterface" + ], + [ + "Profile", + "ProfileDomainInterface" + ], + [ + "Profile", + "ProfileInterface" + ], + [ + "ProfileInterface", + "ProfileDomainInterface" + ] + ], + "external": [ + "composableArchitecture", + "kingfisher", + "tcaFlow" + ] + }, + { + "name": "Web", + "layer": "Feature", + "path": "Projects/Feature/Web/Project.swift", + "hasInterface": true, + "edges": [ + [ + "Web", + "PickeDesignKit" + ], + [ + "Web", + "WebInterface" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "API", + "layer": "Service", + "path": "Projects/Service/API/Project.swift", + "hasInterface": false, + "edges": [ + [ + "API", + "PickeNetworkInterface" + ] + ], + "external": [] + }, + { + "name": "APIEndpoint", + "layer": "Service", + "path": "Projects/Service/APIEndpoint/Project.swift", + "hasInterface": false, + "edges": [ + [ + "APIEndpoint", + "API" + ], + [ + "APIEndpoint", + "AuthDomainInterface" + ], + [ + "APIEndpoint", + "PickeNetworkInterface" + ] + ], + "external": [ + "alamofire" + ] + }, + { + "name": "AudioPlayerService", + "layer": "Service", + "path": "Projects/Service/AudioPlayerService/Project.swift", + "hasInterface": true, + "edges": [ + [ + "AudioPlayerService", + "AudioPlayerServiceInterface" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "DeviceService", + "layer": "Service", + "path": "Projects/Service/DeviceService/Project.swift", + "hasInterface": true, + "edges": [ + [ + "DeviceService", + "APIEndpoint" + ], + [ + "DeviceService", + "DeviceServiceInterface" + ], + [ + "DeviceService", + "PickeCoreLogger" + ], + [ + "DeviceService", + "PickeNetwork" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "PickeAnalytics", + "layer": "Service", + "path": "Projects/Service/PickeAnalytics/Project.swift", + "hasInterface": true, + "edges": [ + [ + "PickeAnalytics", + "PickeAnalyticsInterface" + ], + [ + "PickeAnalytics", + "PickeCoreLogger" + ], + [ + "PickeAnalytics", + "PickeNetwork" + ] + ], + "external": [ + "composableArchitecture", + "mixpanel", + "mixpanelSessionReplay", + "sentry", + "sentrySwiftUI" + ] + }, + { + "name": "PickeAuth", + "layer": "Service", + "path": "Projects/Service/PickeAuth/Project.swift", + "hasInterface": true, + "edges": [ + [ + "PickeAuth", + "APIEndpoint" + ], + [ + "PickeAuth", + "PickeAuthInterface" + ], + [ + "PickeAuth", + "PickeCoreLogger" + ], + [ + "PickeAuth", + "PickeNetwork" + ], + [ + "PickeAuth", + "PickeStorage" + ], + [ + "PickeAuth", + "PickeStorageInterface" + ], + [ + "PickeAuthInterface", + "PickeNetworkInterface" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "PickeConfig", + "layer": "Service", + "path": "Projects/Service/PickeConfig/Project.swift", + "hasInterface": false, + "edges": [], + "external": [ + "firebaseCrashlytics" + ] + }, + { + "name": "ServiceAssembly", + "layer": "Service", + "path": "Projects/Service/ServiceAssembly/Project.swift", + "hasInterface": false, + "edges": [ + [ + "ServiceAssembly", + "API" + ], + [ + "ServiceAssembly", + "APIEndpoint" + ], + [ + "ServiceAssembly", + "AudioPlayerService" + ], + [ + "ServiceAssembly", + "CoreAssembly" + ], + [ + "ServiceAssembly", + "DeviceService" + ], + [ + "ServiceAssembly", + "PickeAnalytics" + ], + [ + "ServiceAssembly", + "PickeAuth" + ], + [ + "ServiceAssembly", + "PickeAuthInterface" + ], + [ + "ServiceAssembly", + "PickeConfig" + ] + ], + "external": [] + }, + { + "name": "PickeAnimation", + "layer": "UI", + "path": "Projects/UI/PickeAnimation/Project.swift", + "hasInterface": false, + "edges": [], + "external": [ + "sdwebImageCore" + ] + }, + { + "name": "PickeDesignKit", + "layer": "UI", + "path": "Projects/UI/PickeDesignKit/Project.swift", + "hasInterface": false, + "edges": [ + [ + "PickeDesignKit", + "PickeCoreUI" + ] + ], + "external": [ + "composableArchitecture" + ] + }, + { + "name": "PickeSharedUI", + "layer": "UI", + "path": "Projects/UI/PickeSharedUI/Project.swift", + "hasInterface": false, + "edges": [ + [ + "PickeSharedUI", + "PickeDesignKit" + ] + ], + "external": [ + "composableArchitecture", + "kingfisher" + ] + } +] diff --git a/docs/diagrams/picke-ads.delivery.json b/docs/diagrams/picke-ads.delivery.json new file mode 100644 index 00000000..d420f27c --- /dev/null +++ b/docs/diagrams/picke-ads.delivery.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "ok": true, + "command": "deliver", + "type": "architecture", + "input": "/Users/suhwonji/Desktop/SideProject/Picke-iOS/docs/diagrams/picke-ads.json", + "output": "/Users/suhwonji/Desktop/SideProject/Picke-iOS/docs/diagrams/picke-ads.html", + "specification": { + "sha256": "cf441ac464f45ed1c805b6ced887a7c2c27285f039f6722e4f2fd2f50703cbc8", + "bytes": 3317 + }, + "artifact": { + "sha256": "49142d10900915a649efe0f57254d0efff112419ae526afbbe9c7ff80e3e202e", + "bytes": 803436 + }, + "validation": { + "checksPassed": 9, + "checkCount": 9, + "compositionProfile": "showcase", + "compositionStatus": "pass", + "errors": 0, + "warnings": 0 + } +} diff --git a/docs/diagrams/picke-ads.html b/docs/diagrams/picke-ads.html new file mode 100644 index 00000000..512a4f5b --- /dev/null +++ b/docs/diagrams/picke-ads.html @@ -0,0 +1,14938 @@ + + + + + + + Picke 광고 배치와 통신 구조 Diagram + + + + + + + + +
+ +
+
+
+

Picke 광고 배치와 통신 구조

+
+
+ + + + + + + +
+ + Picke 광고 배치와 통신 구조 + An architecture diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 탐색 · Hifi · 첫 Kakao 이후 3개마다 서버 광고 · Architecture component + + + + 탐색 · Hifi + 첫 Kakao 이후 3개마다 서버 광고 + + + + 큐레이션 · 마이페이지 · MixedNativeAdView · 교대 표시 · Architecture component + + + + 큐레이션 · 마이페이지 + MixedNativeAdView · 교대 표시 + + + + 홈 · 기존 Kakao 광고 유지 · Architecture component + + + + 홈 + 기존 Kakao 광고 유지 + + + + Feature/Ad · FeedAdRow · 실제 가시성 판단 · Architecture component + + + + Feature/Ad + FeedAdRow · 실제 가시성 판단 + + + + Kakao AdFit · 배너 · 네이티브 광고 · Architecture component + + + + Kakao AdFit + 배너 · 네이티브 광고 + + + + AdDomain · UseCase → Repository · Architecture component + + + + AdDomain + UseCase → Repository + + + + Picke 광고 API · GET 광고 · POST 노출 집계 · Architecture component + + + + Picke 광고 API + GET 광고 · POST 노출 집계 + + + + + + 서버 광고 표시 + + + + 서버 차례 + + + + Kakao 차례 / 조회 실패 + + + + 기존 표시 + + + + 조회 · 가시성 통지 + + + + networkClient · HTTPS + + + + + + + + +

+ + + + + + + + + +
+ + +
+
+
+
+

탐색 배치

+
+
    +
  • • 콘텐츠 3개 → Kakao → 콘텐츠 3개 → 서버 광고
  • +
  • • 서버 광고 목록 소진 시 순환
  • +
+
+ +
+
+
+

노출 집계

+
+
    +
  • • 조회만으로 집계하지 않음
  • +
  • • 화면과 교차하고 앱이 활성일 때 codes 전송
  • +
+
+ +
+
+
+

검증 범위

+
+
    +
  • • 현재 작업 트리의 구현 구조
  • +
  • • 실기기 표시 · 전체 빌드는 미검증
  • +
+
+
+ +
+ + + + diff --git a/docs/diagrams/picke-ads.json b/docs/diagrams/picke-ads.json new file mode 100644 index 00000000..2c9d39c7 --- /dev/null +++ b/docs/diagrams/picke-ads.json @@ -0,0 +1,182 @@ +{ + "schema_version": 1, + "diagram_type": "architecture", + "meta": { + "title": "Picke 광고 배치와 통신 구조", + "quality_profile": "showcase", + "viewBox": [ + 1080, + 560 + ] + }, + "components": [ + { + "id": "explore", + "type": "frontend", + "label": "탐색 · Hifi", + "sublabel": "첫 Kakao 이후 3개마다 서버 광고", + "pos": [ + 40, + 60 + ], + "size": [ + 270, + 90 + ] + }, + { + "id": "mixed", + "type": "frontend", + "label": "큐레이션 · 마이페이지", + "sublabel": "MixedNativeAdView · 교대 표시", + "pos": [ + 40, + 240 + ], + "size": [ + 270, + 90 + ] + }, + { + "id": "home", + "type": "frontend", + "label": "홈", + "sublabel": "기존 Kakao 광고 유지", + "pos": [ + 40, + 420 + ], + "size": [ + 270, + 90 + ] + }, + { + "id": "adui", + "type": "frontend", + "label": "Feature/Ad", + "sublabel": "FeedAdRow · 실제 가시성 판단", + "pos": [ + 400, + 60 + ], + "size": [ + 270, + 90 + ] + }, + { + "id": "kakao", + "type": "external", + "label": "Kakao AdFit", + "sublabel": "배너 · 네이티브 광고", + "pos": [ + 400, + 420 + ], + "size": [ + 270, + 90 + ] + }, + { + "id": "domain", + "type": "backend", + "label": "AdDomain", + "sublabel": "UseCase → Repository", + "pos": [ + 760, + 60 + ], + "size": [ + 270, + 90 + ] + }, + { + "id": "server", + "type": "external", + "label": "Picke 광고 API", + "sublabel": "GET 광고 · POST 노출 집계", + "pos": [ + 760, + 300 + ], + "size": [ + 270, + 90 + ] + } + ], + "connections": [ + { + "from": "explore", + "to": "adui", + "label": "서버 광고 표시" + }, + { + "from": "mixed", + "to": "adui", + "label": "서버 차례" + }, + { + "from": "mixed", + "to": "kakao", + "label": "Kakao 차례 / 조회 실패" + }, + { + "from": "home", + "to": "kakao", + "label": "기존 표시", + "labelAt": [ + 355, + 510 + ] + }, + { + "from": "adui", + "to": "domain", + "label": "조회 · 가시성 통지", + "labelAt": [ + 715, + 170 + ] + }, + { + "from": "domain", + "to": "server", + "label": "networkClient · HTTPS", + "labelAt": [ + 895, + 220 + ] + } + ], + "cards": [ + { + "dot": "cyan", + "title": "탐색 배치", + "items": [ + "콘텐츠 3개 → Kakao → 콘텐츠 3개 → 서버 광고", + "서버 광고 목록 소진 시 순환" + ] + }, + { + "dot": "emerald", + "title": "노출 집계", + "items": [ + "조회만으로 집계하지 않음", + "화면과 교차하고 앱이 활성일 때 codes 전송" + ] + }, + { + "dot": "amber", + "title": "검증 범위", + "items": [ + "현재 작업 트리의 구현 구조", + "실기기 표시 · 전체 빌드는 미검증" + ] + } + ] +} diff --git a/docs/diagrams/picke-ads.visual-check.1440x900.dark.png b/docs/diagrams/picke-ads.visual-check.1440x900.dark.png new file mode 100644 index 00000000..0b1a78d4 Binary files /dev/null and b/docs/diagrams/picke-ads.visual-check.1440x900.dark.png differ diff --git a/docs/diagrams/picke-ads.visual-check.1440x900.light.png b/docs/diagrams/picke-ads.visual-check.1440x900.light.png new file mode 100644 index 00000000..f5a2c665 Binary files /dev/null and b/docs/diagrams/picke-ads.visual-check.1440x900.light.png differ diff --git a/docs/diagrams/picke-ads.visual-check.2048x1320.dark.png b/docs/diagrams/picke-ads.visual-check.2048x1320.dark.png new file mode 100644 index 00000000..f5ec5638 Binary files /dev/null and b/docs/diagrams/picke-ads.visual-check.2048x1320.dark.png differ diff --git a/docs/diagrams/picke-ads.visual-check.2048x1320.light.png b/docs/diagrams/picke-ads.visual-check.2048x1320.light.png new file mode 100644 index 00000000..af7c9156 Binary files /dev/null and b/docs/diagrams/picke-ads.visual-check.2048x1320.light.png differ diff --git a/docs/diagrams/picke-ads.visual-check.html b/docs/diagrams/picke-ads.visual-check.html new file mode 100644 index 00000000..63c2e8f1 --- /dev/null +++ b/docs/diagrams/picke-ads.visual-check.html @@ -0,0 +1,32 @@ + + + + + +Archify automated browser evidence · picke-ads.html + + + +

Automated browser evidence

picke-ads.html · visual-check containment pass · perceptual visual review pending

+
+
+ light 1440 by 900 +
LIGHT · 1440×900 · containment pass
+
+
+ dark 1440 by 900 +
DARK · 1440×900 · containment pass
+
+
+ light 2048 by 1320 +
LIGHT · 2048×1320 · containment pass
+
+
+ dark 2048 by 1320 +
DARK · 2048×1320 · containment pass
+
+
+ + diff --git a/docs/diagrams/picke-ads.visual-check.json b/docs/diagrams/picke-ads.visual-check.json new file mode 100644 index 00000000..839588ec --- /dev/null +++ b/docs/diagrams/picke-ads.visual-check.json @@ -0,0 +1,548 @@ +{ + "schemaVersion": 1, + "ok": true, + "command": "visual-check", + "evidenceKind": "automated-browser", + "status": "pass", + "visualReview": "pending", + "artifact": { + "path": "/Users/suhwonji/Desktop/SideProject/Picke-iOS/docs/diagrams/picke-ads.html", + "sha256": "49142d10900915a649efe0f57254d0efff112419ae526afbbe9c7ff80e3e202e", + "bytes": 803436 + }, + "state": { + "detail": "read", + "motion": "still" + }, + "chrome": { + "status": "available", + "executable": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" + }, + "diagnostics": [], + "containment": { + "status": "pass", + "viewports": [ + { + "width": 1440, + "height": 900, + "theme": "light", + "innerWidth": 1440, + "innerHeight": 900, + "scrollWidth": 1440, + "scrollHeight": 900, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1280, + "diagramWidth": 1250, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 1600, + "height": 1000, + "theme": "light", + "innerWidth": 1600, + "innerHeight": 1000, + "scrollWidth": 1600, + "scrollHeight": 1000, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1326, + "diagramWidth": 1296, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 1920, + "height": 1080, + "theme": "light", + "innerWidth": 1920, + "innerHeight": 1080, + "scrollWidth": 1920, + "scrollHeight": 1080, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1480, + "diagramWidth": 1450, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 2048, + "height": 1320, + "theme": "light", + "innerWidth": 2048, + "innerHeight": 1320, + "scrollWidth": 2048, + "scrollHeight": 1320, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1882, + "diagramWidth": 1832, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 41, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + } + ] + }, + "readability": { + "status": "pass", + "minimumProjectedNodeTextPx": 6, + "viewports": [ + { + "width": 1440, + "height": 900, + "theme": "light", + "innerWidth": 1440, + "innerHeight": 900, + "scrollWidth": 1440, + "scrollHeight": 900, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1280, + "diagramWidth": 1250, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 1600, + "height": 1000, + "theme": "light", + "innerWidth": 1600, + "innerHeight": 1000, + "scrollWidth": 1600, + "scrollHeight": 1000, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1326, + "diagramWidth": 1296, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 1920, + "height": 1080, + "theme": "light", + "innerWidth": 1920, + "innerHeight": 1080, + "scrollWidth": 1920, + "scrollHeight": 1080, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1480, + "diagramWidth": 1450, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 2048, + "height": 1320, + "theme": "light", + "innerWidth": 2048, + "innerHeight": 1320, + "scrollWidth": 2048, + "scrollHeight": 1320, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1882, + "diagramWidth": 1832, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 41, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + } + ] + }, + "viewerChrome": { + "status": "pass", + "viewports": [ + { + "width": 1440, + "height": 900, + "theme": "light", + "innerWidth": 1440, + "innerHeight": 900, + "scrollWidth": 1440, + "scrollHeight": 900, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1280, + "diagramWidth": 1250, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 1600, + "height": 1000, + "theme": "light", + "innerWidth": 1600, + "innerHeight": 1000, + "scrollWidth": 1600, + "scrollHeight": 1000, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1326, + "diagramWidth": 1296, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 1920, + "height": 1080, + "theme": "light", + "innerWidth": 1920, + "innerHeight": 1080, + "scrollWidth": 1920, + "scrollHeight": 1080, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1480, + "diagramWidth": 1450, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 2048, + "height": 1320, + "theme": "light", + "innerWidth": 2048, + "innerHeight": 1320, + "scrollWidth": 2048, + "scrollHeight": 1320, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1882, + "diagramWidth": 1832, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 41, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + } + ] + }, + "captures": { + "status": "pass", + "screenshots": [ + { + "width": 1440, + "height": 900, + "theme": "light", + "innerWidth": 1440, + "innerHeight": 900, + "scrollWidth": 1440, + "scrollHeight": 900, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1280, + "diagramWidth": 1250, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light", + "file": "picke-ads.visual-check.1440x900.light.png" + }, + { + "width": 1440, + "height": 900, + "theme": "dark", + "innerWidth": 1440, + "innerHeight": 900, + "scrollWidth": 1440, + "scrollHeight": 900, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1280, + "diagramWidth": 1250, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "dark", + "file": "picke-ads.visual-check.1440x900.dark.png" + }, + { + "width": 2048, + "height": 1320, + "theme": "light", + "innerWidth": 2048, + "innerHeight": 1320, + "scrollWidth": 2048, + "scrollHeight": 1320, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1882, + "diagramWidth": 1832, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 41, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light", + "file": "picke-ads.visual-check.2048x1320.light.png" + }, + { + "width": 2048, + "height": 1320, + "theme": "dark", + "innerWidth": 2048, + "innerHeight": 1320, + "scrollWidth": 2048, + "scrollHeight": 1320, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1882, + "diagramWidth": 1832, + "viewBoxWidth": 1080, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "첫 Kakao 이후 3개마다 서버 광고", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": false, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 41, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "dark", + "file": "picke-ads.visual-check.2048x1320.dark.png" + } + ], + "contactSheet": "picke-ads.visual-check.html" + }, + "sidecars": { + "receipt": "picke-ads.visual-check.json", + "contactSheet": "picke-ads.visual-check.html" + } +} diff --git a/docs/diagrams/picke-domains-architecture.json b/docs/diagrams/picke-domains-architecture.json new file mode 100644 index 00000000..48e731aa --- /dev/null +++ b/docs/diagrams/picke-domains-architecture.json @@ -0,0 +1,273 @@ +{ + "schema_version": 1, + "diagram_type": "architecture", + "meta": { + "title": "Picke 전체 도메인 지도", + "quality_profile": "showcase", + "viewBox": [ + 1100, + 745 + ] + }, + "components": [ + { + "id": "domain-appupdate", + "type": "backend", + "label": "AppUpdateDomain", + "sublabel": "앱 업데이트", + "pos": [ + 40, + 250 + ], + "size": [ + 300, + 100 + ], + "tag": "App" + }, + { + "id": "domain-attendance", + "type": "backend", + "label": "AttendanceDomain", + "sublabel": "출석", + "pos": [ + 40, + 415 + ], + "size": [ + 300, + 100 + ], + "tag": "Home" + }, + { + "id": "domain-auth", + "type": "backend", + "label": "AuthDomain", + "sublabel": "인증 · OAuth", + "pos": [ + 760, + 415 + ], + "size": [ + 300, + 100 + ], + "tag": "Auth · Home · Profile" + }, + { + "id": "domain-battle", + "type": "backend", + "label": "BattleDomain", + "sublabel": "배틀", + "pos": [ + 760, + 85 + ], + "size": [ + 300, + 100 + ], + "tag": "Battle · Chat · Hifi · Home · Profile" + }, + { + "id": "domain-comment", + "type": "backend", + "label": "CommentDomain", + "sublabel": "댓글", + "pos": [ + 400, + 85 + ], + "size": [ + 300, + 100 + ], + "tag": "Chat" + }, + { + "id": "domain-ad", + "type": "backend", + "label": "AdDomain", + "sublabel": "광고", + "pos": [ + 400, + 415 + ], + "size": [ + 300, + 100 + ], + "tag": "Ad · Hifi" + }, + { + "id": "domain-home", + "type": "backend", + "label": "HomeDomain", + "sublabel": "홈", + "pos": [ + 760, + 250 + ], + "size": [ + 300, + 100 + ], + "tag": "Chat · Hifi · Home" + }, + { + "id": "domain-notification", + "type": "backend", + "label": "NotificationDomain", + "sublabel": "알림", + "pos": [ + 400, + 580 + ], + "size": [ + 300, + 100 + ], + "tag": "Notification · Hifi · Home · Profile" + }, + { + "id": "domain-perspective", + "type": "backend", + "label": "PerspectiveDomain", + "sublabel": "관점", + "pos": [ + 40, + 85 + ], + "size": [ + 300, + 100 + ], + "tag": "Chat" + }, + { + "id": "domain-profile", + "type": "backend", + "label": "ProfileDomain", + "sublabel": "프로필", + "pos": [ + 40, + 580 + ], + "size": [ + 300, + 100 + ], + "tag": "Profile" + }, + { + "id": "domain-search", + "type": "backend", + "label": "SearchDomain", + "sublabel": "탐색 · 검색", + "pos": [ + 400, + 250 + ], + "size": [ + 300, + 100 + ], + "tag": "Hifi" + }, + { + "id": "domain-domainassembly", + "type": "backend", + "label": "DomainAssembly", + "sublabel": "도메인 조립", + "pos": [ + 760, + 580 + ], + "size": [ + 300, + 100 + ], + "tag": "App → 11개 도메인 구현체" + } + ], + "cards": [ + { + "dot": "cyan", + "title": "읽는 방법", + "items": [ + "화살표: 도메인이 참조하는 Interface", + "노드 태그: 직접 사용하는 Feature / App" + ] + }, + { + "dot": "emerald", + "title": "앱 조립", + "items": [ + "App → DomainAssembly → 11개 구현체", + "각 LiveDependencies에서 UseCase · Repository 연결" + ] + }, + { + "dot": "amber", + "title": "범위", + "items": [ + "Project.swift 직접 의존 기준 · 전이 의존 제외", + "런타임 호출 순서가 아닌 모듈 의존 지도" + ] + } + ], + "connections": [ + { + "from": "domain-perspective", + "to": "domain-comment", + "label": "Interface", + "fromSide": "right", + "toSide": "left" + }, + { + "from": "domain-perspective", + "to": "domain-battle", + "label": "Interface", + "fromSide": "top", + "toSide": "top" + }, + { + "from": "domain-comment", + "to": "domain-battle", + "label": "Interface", + "fromSide": "right", + "toSide": "left" + }, + { + "from": "domain-battle", + "to": "domain-home", + "label": "Interface", + "fromSide": "bottom", + "toSide": "top", + "labelDy": 60 + }, + { + "from": "domain-search", + "to": "domain-battle", + "label": "Interface", + "fromSide": "top", + "toSide": "bottom" + }, + { + "from": "domain-search", + "to": "domain-home", + "label": "Interface", + "fromSide": "right", + "toSide": "left" + }, + { + "from": "domain-home", + "to": "domain-auth", + "label": "Interface", + "fromSide": "bottom", + "toSide": "top", + "labelDy": 40 + } + ] +} diff --git a/docs/diagrams/picke-domains.delivery.json b/docs/diagrams/picke-domains.delivery.json new file mode 100644 index 00000000..e359a1b9 --- /dev/null +++ b/docs/diagrams/picke-domains.delivery.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "ok": true, + "command": "deliver", + "type": "architecture", + "input": "/Users/suhwonji/Desktop/SideProject/Picke-iOS/docs/diagrams/picke-domains-architecture.json", + "output": "/Users/suhwonji/Desktop/SideProject/Picke-iOS/docs/diagrams/picke-domains.html", + "specification": { + "sha256": "e51e6bab05e08e325e1ab2377a130e122abc46882851a77c93cd6a44d764b4c1", + "bytes": 5154 + }, + "artifact": { + "sha256": "975881935c4c90540c6860ed6ada2b5ee2ba3c1db4217f7eb8955baf8bb212a3", + "bytes": 810844 + }, + "validation": { + "checksPassed": 9, + "checkCount": 9, + "compositionProfile": "showcase", + "compositionStatus": "pass", + "errors": 0, + "warnings": 0 + } +} diff --git a/docs/diagrams/picke-domains.html b/docs/diagrams/picke-domains.html new file mode 100644 index 00000000..9d9a6ae5 --- /dev/null +++ b/docs/diagrams/picke-domains.html @@ -0,0 +1,15002 @@ + + + + + + + Picke 전체 도메인 지도 Diagram + + + + + + + + +
+ +
+
+
+

Picke 전체 도메인 지도

+
+
+ + + + + + + +
+ + Picke 전체 도메인 지도 + An architecture diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AppUpdateDomain · 앱 업데이트 · Architecture component · App + + + + AppUpdateDomain + 앱 업데이트 + App + + + + AttendanceDomain · 출석 · Architecture component · Home + + + + AttendanceDomain + 출석 + Home + + + + AuthDomain · 인증 · OAuth · Architecture component · Auth · Home · Profile + + + + AuthDomain + 인증 · OAuth + Auth · Home · Profile + + + + BattleDomain · 배틀 · Architecture component · Battle · Chat · Hifi · Home · Profile + + + + BattleDomain + 배틀 + Battle · Chat · Hifi · Home · Profile + + + + CommentDomain · 댓글 · Architecture component · Chat + + + + CommentDomain + 댓글 + Chat + + + + AdDomain · 광고 · Architecture component · Ad · Hifi + + + + AdDomain + 광고 + Ad · Hifi + + + + HomeDomain · 홈 · Architecture component · Chat · Hifi · Home + + + + HomeDomain + 홈 + Chat · Hifi · Home + + + + NotificationDomain · 알림 · Architecture component · Notification · Hifi · Home · Profile + + + + NotificationDomain + 알림 + Notification · Hifi · Home · Profile + + + + PerspectiveDomain · 관점 · Architecture component · Chat + + + + PerspectiveDomain + 관점 + Chat + + + + ProfileDomain · 프로필 · Architecture component · Profile + + + + ProfileDomain + 프로필 + Profile + + + + SearchDomain · 탐색 · 검색 · Architecture component · Hifi + + + + SearchDomain + 탐색 · 검색 + Hifi + + + + DomainAssembly · 도메인 조립 · Architecture component · App → 11개 도메인 구현체 + + + + DomainAssembly + 도메인 조립 + App → 11개 도메인 구현체 + + + + + + Interface + + + + Interface + + + + Interface + + + + Interface + + + + Interface + + + + Interface + + + + Interface + + + + + + + + Legend + + + Backend + + + +

+ + + + + + + + + +
+ + +
+
+
+
+

읽는 방법

+
+
    +
  • • 화살표: 도메인이 참조하는 Interface
  • +
  • • 노드 태그: 직접 사용하는 Feature / App
  • +
+
+ +
+
+
+

앱 조립

+
+
    +
  • • App → DomainAssembly → 11개 구현체
  • +
  • • 각 LiveDependencies에서 UseCase · Repository 연결
  • +
+
+ +
+
+
+

범위

+
+
    +
  • • Project.swift 직접 의존 기준 · 전이 의존 제외
  • +
  • • 런타임 호출 순서가 아닌 모듈 의존 지도
  • +
+
+
+ +
+ + + + diff --git a/docs/diagrams/picke-domains.json b/docs/diagrams/picke-domains.json new file mode 100644 index 00000000..8f140880 --- /dev/null +++ b/docs/diagrams/picke-domains.json @@ -0,0 +1,208 @@ +{ + "schema_version": 1, + "diagram_type": "architecture", + "meta": { + "title": "Picke 전체 도메인 지도", + "quality_profile": "showcase", + "viewBox": [ + 1100, + 665 + ] + }, + "components": [ + { + "id": "domain-appupdate", + "type": "backend", + "label": "AppUpdateDomain", + "sublabel": "앱 업데이트", + "pos": [ + 40, + 45 + ], + "size": [ + 300, + 100 + ] + }, + { + "id": "domain-attendance", + "type": "backend", + "label": "AttendanceDomain", + "sublabel": "출석", + "pos": [ + 400, + 45 + ], + "size": [ + 300, + 100 + ] + }, + { + "id": "domain-auth", + "type": "backend", + "label": "AuthDomain", + "sublabel": "인증 · OAuth", + "pos": [ + 760, + 45 + ], + "size": [ + 300, + 100 + ] + }, + { + "id": "domain-battle", + "type": "backend", + "label": "BattleDomain", + "sublabel": "배틀", + "pos": [ + 40, + 195 + ], + "size": [ + 300, + 100 + ] + }, + { + "id": "domain-comment", + "type": "backend", + "label": "CommentDomain", + "sublabel": "댓글", + "pos": [ + 400, + 195 + ], + "size": [ + 300, + 100 + ] + }, + { + "id": "domain-ad", + "type": "backend", + "label": "AdDomain", + "sublabel": "광고", + "pos": [ + 760, + 195 + ], + "size": [ + 300, + 100 + ] + }, + { + "id": "domain-home", + "type": "backend", + "label": "HomeDomain", + "sublabel": "홈", + "pos": [ + 40, + 345 + ], + "size": [ + 300, + 100 + ] + }, + { + "id": "domain-notification", + "type": "backend", + "label": "NotificationDomain", + "sublabel": "알림", + "pos": [ + 400, + 345 + ], + "size": [ + 300, + 100 + ] + }, + { + "id": "domain-perspective", + "type": "backend", + "label": "PerspectiveDomain", + "sublabel": "관점", + "pos": [ + 760, + 345 + ], + "size": [ + 300, + 100 + ] + }, + { + "id": "domain-profile", + "type": "backend", + "label": "ProfileDomain", + "sublabel": "프로필", + "pos": [ + 40, + 495 + ], + "size": [ + 300, + 100 + ] + }, + { + "id": "domain-search", + "type": "backend", + "label": "SearchDomain", + "sublabel": "탐색 · 검색", + "pos": [ + 400, + 495 + ], + "size": [ + 300, + 100 + ] + }, + { + "id": "domain-domainassembly", + "type": "backend", + "label": "DomainAssembly", + "sublabel": "도메인 조립", + "pos": [ + 760, + 495 + ], + "size": [ + 300, + 100 + ] + } + ], + "cards": [ + { + "dot": "cyan", + "title": "구성", + "items": [ + "11개 기능 도메인 + DomainAssembly", + "현재 작업 트리 기준" + ] + }, + { + "dot": "emerald", + "title": "의존 관계", + "items": [ + "Feature → Domain Interface", + "App → DomainAssembly → 구현체" + ] + }, + { + "dot": "amber", + "title": "통신 흐름", + "items": [ + "UseCase → Repository → networkClient", + "실행 검증이 아닌 코드 구조 지도" + ] + } + ] +} diff --git a/docs/diagrams/picke-domains.visual-check.1440x900.dark.png b/docs/diagrams/picke-domains.visual-check.1440x900.dark.png new file mode 100644 index 00000000..a4b161ef Binary files /dev/null and b/docs/diagrams/picke-domains.visual-check.1440x900.dark.png differ diff --git a/docs/diagrams/picke-domains.visual-check.1440x900.light.png b/docs/diagrams/picke-domains.visual-check.1440x900.light.png new file mode 100644 index 00000000..e7df15c2 Binary files /dev/null and b/docs/diagrams/picke-domains.visual-check.1440x900.light.png differ diff --git a/docs/diagrams/picke-domains.visual-check.2048x1320.dark.png b/docs/diagrams/picke-domains.visual-check.2048x1320.dark.png new file mode 100644 index 00000000..c349569d Binary files /dev/null and b/docs/diagrams/picke-domains.visual-check.2048x1320.dark.png differ diff --git a/docs/diagrams/picke-domains.visual-check.2048x1320.light.png b/docs/diagrams/picke-domains.visual-check.2048x1320.light.png new file mode 100644 index 00000000..387de37b Binary files /dev/null and b/docs/diagrams/picke-domains.visual-check.2048x1320.light.png differ diff --git a/docs/diagrams/picke-domains.visual-check.html b/docs/diagrams/picke-domains.visual-check.html new file mode 100644 index 00000000..ed739f64 --- /dev/null +++ b/docs/diagrams/picke-domains.visual-check.html @@ -0,0 +1,32 @@ + + + + + +Archify automated browser evidence · picke-domains.html + + + +

Automated browser evidence

picke-domains.html · visual-check containment fail · perceptual visual review pending

+
+
+ light 1440 by 900 +
LIGHT · 1440×900 · containment fail
+
+
+ dark 1440 by 900 +
DARK · 1440×900 · containment fail
+
+
+ light 2048 by 1320 +
LIGHT · 2048×1320 · containment pass
+
+
+ dark 2048 by 1320 +
DARK · 2048×1320 · containment pass
+
+
+ + diff --git a/docs/diagrams/picke-domains.visual-check.json b/docs/diagrams/picke-domains.visual-check.json new file mode 100644 index 00000000..f58d86c3 --- /dev/null +++ b/docs/diagrams/picke-domains.visual-check.json @@ -0,0 +1,645 @@ +{ + "schemaVersion": 1, + "ok": false, + "command": "visual-check", + "evidenceKind": "automated-browser", + "status": "fail", + "visualReview": "pending", + "artifact": { + "path": "/Users/suhwonji/Desktop/SideProject/Picke-iOS/docs/diagrams/picke-domains.html", + "sha256": "975881935c4c90540c6860ed6ada2b5ee2ba3c1db4217f7eb8955baf8bb212a3", + "bytes": 810844 + }, + "state": { + "detail": "read", + "motion": "still" + }, + "chrome": { + "status": "available", + "executable": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" + }, + "diagnostics": [ + { + "code": "viewer/viewport-overflow", + "severity": "error", + "message": "The rendered artifact overflows the 1440x900 light viewport.", + "subject": { + "artifact": "/Users/suhwonji/Desktop/SideProject/Picke-iOS/docs/diagrams/picke-domains.html", + "viewport": { + "width": 1440, + "height": 900, + "theme": "light" + } + }, + "evidence": { + "innerWidth": 1440, + "innerHeight": 900, + "scrollWidth": 1440, + "scrollHeight": 1152, + "overflowX": false, + "overflowY": true + }, + "supportedFixes": [ + "contain the rendered layout within 1440x900, then rerun visual-check" + ] + }, + { + "code": "viewer/viewport-overflow", + "severity": "error", + "message": "The rendered artifact overflows the 1600x1000 light viewport.", + "subject": { + "artifact": "/Users/suhwonji/Desktop/SideProject/Picke-iOS/docs/diagrams/picke-domains.html", + "viewport": { + "width": 1600, + "height": 1000, + "theme": "light" + } + }, + "evidence": { + "innerWidth": 1600, + "innerHeight": 1000, + "scrollWidth": 1600, + "scrollHeight": 1271, + "overflowX": false, + "overflowY": true + }, + "supportedFixes": [ + "contain the rendered layout within 1600x1000, then rerun visual-check" + ] + }, + { + "code": "viewer/viewport-overflow", + "severity": "error", + "message": "The rendered artifact overflows the 1920x1080 light viewport.", + "subject": { + "artifact": "/Users/suhwonji/Desktop/SideProject/Picke-iOS/docs/diagrams/picke-domains.html", + "viewport": { + "width": 1920, + "height": 1080, + "theme": "light" + } + }, + "evidence": { + "innerWidth": 1920, + "innerHeight": 1080, + "scrollWidth": 1920, + "scrollHeight": 1271, + "overflowX": false, + "overflowY": true + }, + "supportedFixes": [ + "contain the rendered layout within 1920x1080, then rerun visual-check" + ] + }, + { + "code": "viewer/viewport-overflow", + "severity": "error", + "message": "The rendered artifact overflows the 1440x900 dark viewport.", + "subject": { + "artifact": "/Users/suhwonji/Desktop/SideProject/Picke-iOS/docs/diagrams/picke-domains.html", + "viewport": { + "width": 1440, + "height": 900, + "theme": "dark" + } + }, + "evidence": { + "innerWidth": 1440, + "innerHeight": 900, + "scrollWidth": 1440, + "scrollHeight": 1152, + "overflowX": false, + "overflowY": true + }, + "supportedFixes": [ + "contain the rendered layout within 1440x900, then rerun visual-check" + ] + } + ], + "containment": { + "status": "fail", + "viewports": [ + { + "width": 1440, + "height": 900, + "theme": "light", + "innerWidth": 1440, + "innerHeight": 900, + "scrollWidth": 1440, + "scrollHeight": 1152, + "overflowX": false, + "overflowY": true, + "ok": false, + "readerWidth": 1376, + "diagramWidth": 1346, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 1600, + "height": 1000, + "theme": "light", + "innerWidth": 1600, + "innerHeight": 1000, + "scrollWidth": 1600, + "scrollHeight": 1271, + "overflowX": false, + "overflowY": true, + "ok": false, + "readerWidth": 1440, + "diagramWidth": 1410, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 1920, + "height": 1080, + "theme": "light", + "innerWidth": 1920, + "innerHeight": 1080, + "scrollWidth": 1920, + "scrollHeight": 1271, + "overflowX": false, + "overflowY": true, + "ok": false, + "readerWidth": 1440, + "diagramWidth": 1410, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 2048, + "height": 1320, + "theme": "light", + "innerWidth": 2048, + "innerHeight": 1320, + "scrollWidth": 2048, + "scrollHeight": 1320, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1440, + "diagramWidth": 1390, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 41, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + } + ] + }, + "readability": { + "status": "pass", + "minimumProjectedNodeTextPx": 6, + "viewports": [ + { + "width": 1440, + "height": 900, + "theme": "light", + "innerWidth": 1440, + "innerHeight": 900, + "scrollWidth": 1440, + "scrollHeight": 1152, + "overflowX": false, + "overflowY": true, + "ok": false, + "readerWidth": 1376, + "diagramWidth": 1346, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 1600, + "height": 1000, + "theme": "light", + "innerWidth": 1600, + "innerHeight": 1000, + "scrollWidth": 1600, + "scrollHeight": 1271, + "overflowX": false, + "overflowY": true, + "ok": false, + "readerWidth": 1440, + "diagramWidth": 1410, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 1920, + "height": 1080, + "theme": "light", + "innerWidth": 1920, + "innerHeight": 1080, + "scrollWidth": 1920, + "scrollHeight": 1271, + "overflowX": false, + "overflowY": true, + "ok": false, + "readerWidth": 1440, + "diagramWidth": 1410, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 2048, + "height": 1320, + "theme": "light", + "innerWidth": 2048, + "innerHeight": 1320, + "scrollWidth": 2048, + "scrollHeight": 1320, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1440, + "diagramWidth": 1390, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 41, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + } + ] + }, + "viewerChrome": { + "status": "pass", + "viewports": [ + { + "width": 1440, + "height": 900, + "theme": "light", + "innerWidth": 1440, + "innerHeight": 900, + "scrollWidth": 1440, + "scrollHeight": 1152, + "overflowX": false, + "overflowY": true, + "ok": false, + "readerWidth": 1376, + "diagramWidth": 1346, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 1600, + "height": 1000, + "theme": "light", + "innerWidth": 1600, + "innerHeight": 1000, + "scrollWidth": 1600, + "scrollHeight": 1271, + "overflowX": false, + "overflowY": true, + "ok": false, + "readerWidth": 1440, + "diagramWidth": 1410, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 1920, + "height": 1080, + "theme": "light", + "innerWidth": 1920, + "innerHeight": 1080, + "scrollWidth": 1920, + "scrollHeight": 1271, + "overflowX": false, + "overflowY": true, + "ok": false, + "readerWidth": 1440, + "diagramWidth": 1410, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + }, + { + "width": 2048, + "height": 1320, + "theme": "light", + "innerWidth": 2048, + "innerHeight": 1320, + "scrollWidth": 2048, + "scrollHeight": 1320, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1440, + "diagramWidth": 1390, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 41, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light" + } + ] + }, + "captures": { + "status": "pass", + "screenshots": [ + { + "width": 1440, + "height": 900, + "theme": "light", + "innerWidth": 1440, + "innerHeight": 900, + "scrollWidth": 1440, + "scrollHeight": 1152, + "overflowX": false, + "overflowY": true, + "ok": false, + "readerWidth": 1376, + "diagramWidth": 1346, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light", + "file": "picke-domains.visual-check.1440x900.light.png" + }, + { + "width": 1440, + "height": 900, + "theme": "dark", + "innerWidth": 1440, + "innerHeight": 900, + "scrollWidth": 1440, + "scrollHeight": 1152, + "overflowX": false, + "overflowY": true, + "ok": false, + "readerWidth": 1376, + "diagramWidth": 1346, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 51, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "dark", + "file": "picke-domains.visual-check.1440x900.dark.png" + }, + { + "width": 2048, + "height": 1320, + "theme": "light", + "innerWidth": 2048, + "innerHeight": 1320, + "scrollWidth": 2048, + "scrollHeight": 1320, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1440, + "diagramWidth": 1390, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 41, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "light", + "file": "picke-domains.visual-check.2048x1320.light.png" + }, + { + "width": 2048, + "height": 1320, + "theme": "dark", + "innerWidth": 2048, + "innerHeight": 1320, + "scrollWidth": 2048, + "scrollHeight": 1320, + "overflowX": false, + "overflowY": false, + "ok": true, + "readerWidth": 1440, + "diagramWidth": 1390, + "viewBoxWidth": 1100, + "minimumProjectedNodeTextPx": 9, + "minimumProjectedNodeText": "앱 업데이트", + "minimumProjectedNodeTextDetail": "context", + "minimumRequiredNodeTextPx": 6, + "readabilityOk": true, + "hasLegend": true, + "hasNavigationDock": true, + "legendDockIntersectionArea": 0, + "dockStageIntersectionArea": 0, + "dockStageGap": 10.21875, + "requiredDockStageGap": 10, + "viewerChromeStageOk": true, + "viewerChromeReserve": 41, + "viewerChromeActive": true, + "viewerChromeOk": true, + "resolvedTheme": "dark", + "file": "picke-domains.visual-check.2048x1320.dark.png" + } + ], + "contactSheet": "picke-domains.visual-check.html" + }, + "sidecars": { + "receipt": "picke-domains.visual-check.json", + "contactSheet": "picke-domains.visual-check.html" + } +} diff --git a/make b/make index a12c1085..451b4179 100755 Binary files a/make and b/make differ diff --git a/scripts/generate_module_diagrams.py b/scripts/generate_module_diagrams.py new file mode 100644 index 00000000..af033f3f --- /dev/null +++ b/scripts/generate_module_diagrams.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Render repository target dependencies as GitHub-compatible SVG diagrams.""" + +import argparse +import json +from pathlib import Path +import re +import subprocess + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT = ROOT / "docs/diagrams/modules" +LAYERS = ("App", "Feature", "Domain", "Service", "Core", "UI") +COLORS = dict(zip(LAYERS, ("#ede9fe", "#cffafe", "#d1fae5", "#fef3c7", "#e2e8f0", "#fce7f3"))) +START = "" +END = "" + + +def array(source, name): + match = re.search(r"\b" + name + r"\s*:\s*\[", source) + if not match: + return "" + end = source.index("]", match.end()) + return source[match.end():end] + + +def inventory(): + catalog = (ROOT / "Plugins/DependencyPlugin/ProjectDescriptionHelpers/TargetDependency+Module/Modules.swift").read_text() + names = {} + for layer in LAYERS[1:]: + body = re.search(r"enum " + layer + r"Module\b[^\{]*\{(.*?)\n\}", catalog, re.S)[1] + names[layer.lower()] = dict(re.findall(r'case\s+(\w+)\s*=\s*"([^"]+)"', body)) + + modules = [] + for path in sorted((ROOT / "Projects").glob("**/Project.swift")): + relative = path.relative_to(ROOT) + layer = relative.parts[1] + if layer not in LAYERS: + raise ValueError(f"Unknown project layer: {relative}") + name = "Picke" if layer == "App" else path.parent.name + source = re.sub(r"//[^\n]*", "", path.read_text()) + has_interface = bool(re.search(r"hasInterface:\s*true", source)) + edges = set() + external = set() + for field, origin in (("dependencies", name), ("interfaceDependencies", name + "Interface")): + entries = array(source, field) + for entry in filter(str.strip, entries.splitlines()): + entry = entry.strip().rstrip(",") + if entry.startswith(".SPM."): + external.add(entry.removeprefix(".SPM.")) + continue + alias = re.fullmatch(r"\.(feature|domain|service|core)Assembly", entry) + if alias: + edges.add((origin, alias[1].capitalize() + "Assembly")) + continue + match = re.fullmatch(r"\.(feature|domain|service|core|ui)\(\.(\w+)(?:,\s*\.(\w+))?\)", entry) + if not match: + raise ValueError(f"Unsupported dependency in {relative}: {entry!r}") + dependency_layer, key, target = match.groups() + target = target or ("interface" if dependency_layer in ("feature", "domain") else "implementation") + suffix = {"interface": "Interface", "implementation": "", "testing": "Testing"}[target] + edges.add((origin, names[dependency_layer][key] + suffix)) + if has_interface: + edges.add((name, name + "Interface")) + modules.append(dict(name=name, layer=layer, path=str(relative), hasInterface=has_interface, + edges=sorted(edges), external=sorted(external))) + targets = {m["name"] for m in modules} | {m["name"] + "Interface" for m in modules if m["hasInterface"]} + for module in modules: + for origin, target in module["edges"]: + if origin not in targets or target not in targets: + raise ValueError(f"Undeclared target: {origin} -> {target}") + return modules + + +def diagram(module, modules): + owners = {target: item for item in modules for target in + ([item["name"], item["name"] + "Interface"] if item["hasInterface"] else [item["name"]])} + nodes = {module["name"]} | {node for edge in module["edges"] for node in edge} + quote = json.dumps + lines = ["digraph G {", 'graph [rankdir=LR, bgcolor="#ffffff", pad="0.3", nodesep="0.24", ranksep="0.8"];', + 'node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=13, margin="0.18,0.12", color="#64748b", fontcolor="#0f172a"];', + 'edge [color="#64748b", arrowsize=0.7];'] + for node in sorted(nodes): + owner = owners[node] + interface = node.endswith("Interface") + style = "rounded,filled,dashed" if interface else "rounded,filled" + width = "2" if node == module["name"] else "1" + lines.append(f'{quote(node)} [fillcolor="{COLORS[owner["layer"]]}", style="{style}", penwidth={width}];') + for origin, target in module["edges"]: + lines.append(f"{quote(origin)} -> {quote(target)};") + lines.append("}") + return subprocess.run(["dot", "-Tsvg"], input="\n".join(lines), text=True, + capture_output=True, check=True).stdout + + +def readme_block(modules): + lines = [START, "## 모듈 그래프", "", + f"현재 {len(modules)}개 모듈의 구현·Interface 타깃 의존성을 표시합니다. 각 항목을 펼치면 GitHub에서 SVG 그림을 바로 볼 수 있습니다.", "", + "화살표는 **참조하는 타깃 → 참조되는 타깃**, 점선 테두리는 **Interface**입니다. `Project.swift`의 `dependencies`·`interfaceDependencies`와 템플릿이 연결하는 자기 Interface를 반영합니다. 외부 SPM 패키지는 이름으로 별도 표기하고, Tests·Testing·Demo와 전이 의존성은 생략합니다.", "", + "`APIEndpoint → AuthDomainInterface`처럼 현재 코드에 존재하는 계층 간 참조도 그대로 표시합니다. 실행 순서나 이상적인 아키텍처를 나타내는 그림은 아닙니다.", "", + "[광고 HTML](docs/diagrams/picke-ads.html) · [전체 도메인 HTML](docs/diagrams/picke-domains.html) — 파일을 내려받아 브라우저에서 열면 확대·검색할 수 있습니다.", ""] + for layer in LAYERS: + members = [m for m in modules if m["layer"] == layer] + lines += [f"### {layer} · {len(members)}개", ""] + for module in members: + name = module["name"] + lines += ["
", f"{name}", "", f"[모듈 선언]({module['path']})", "", + f"![{name} 직접 의존 관계](docs/diagrams/modules/{name}.svg)", ""] + if module["external"]: + lines += ["외부 패키지 선언: " + ", ".join(f"`{p}`" for p in module["external"]) + ".", ""] + if not module["edges"]: + lines += ["다른 내부 모듈에 대한 직접 의존성이 없습니다.", ""] + lines += ["
", ""] + lines += ["갱신·검증:", "", "```bash", "python3 scripts/generate_module_diagrams.py", + "python3 scripts/generate_module_diagrams.py --check", "```", "", + "SVG 생성에는 Graphviz의 `dot`이 필요합니다. 앱 빌드나 Tuist 캐시 생성은 실행하지 않습니다.", "", END] + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="Fail if generated diagrams or README are stale") + args = parser.parse_args() + modules = inventory() + generated = {OUTPUT / (m["name"] + ".svg"): diagram(m, modules) for m in modules} + generated[OUTPUT / "manifest.json"] = json.dumps(modules, ensure_ascii=False, indent=2) + "\n" + readme = ROOT / "README.md" + text = readme.read_text() + start = text.index(START) if START in text else text.index("## 모듈 그래프") + end = text.index(END) + len(END) if END in text else text.index("## 기술 스택") + generated[readme] = text[:start] + readme_block(modules) + "\n\n" + text[end:].lstrip("\n") + stale = [str(path.relative_to(ROOT)) for path, content in generated.items() + if not path.exists() or path.read_text() != content] + if args.check: + if stale: + raise SystemExit("Stale diagram outputs:\n" + "\n".join(stale)) + else: + OUTPUT.mkdir(parents=True, exist_ok=True) + for path, content in generated.items(): + path.write_text(content) + print(f"{'Verified' if args.check else 'Generated'} {len(modules)} module diagrams and README references") + + +if __name__ == "__main__": + main() diff --git a/scripts/tests/test_tuist_cache_commands.py b/scripts/tests/test_tuist_cache_commands.py new file mode 100644 index 00000000..56aec6b5 --- /dev/null +++ b/scripts/tests/test_tuist_cache_commands.py @@ -0,0 +1,89 @@ +"""Verify CLI routing without downloading packages or building cache artifacts.""" +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] + + +class CacheCommandsTests(unittest.TestCase): + def run_command(self, arguments, *, ci=False, failure=None): + with tempfile.TemporaryDirectory(prefix="picke-cache-test-") as directory: + folder = Path(directory) + log = folder / "commands.jsonl" + mise = folder / "mise" + mise.write_text( + f"#!{sys.executable}\n" + "import json, os, sys\n" + "args = sys.argv[1:]\n" + "with open(os.environ['COMMAND_LOG'], 'a') as log:\n" + " log.write(json.dumps(args) + '\\n')\n" + "failure = os.environ.get('FAIL_COMMAND', '')\n" + "sys.exit(7 if failure and failure in args else 0)\n" + ) + mise.chmod(0o755) + environment = os.environ.copy() + for key in ("CI", "GITHUB_ACTIONS", "BITRISE_IO", "TUIST_CI"): + environment.pop(key, None) + environment.update(PATH=f"{folder}:{environment['PATH']}", + COMMAND_LOG=str(log), FAIL_COMMAND=failure or "") + if ci: + environment["CI"] = "true" + command = [str(ROOT / "make"), *arguments] + result = subprocess.run(command, cwd=ROOT, env=environment, + capture_output=True, text=True, timeout=60) + calls = [json.loads(line) for line in log.read_text().splitlines()] if log.exists() else [] + return result, [call[3:] for call in calls if call[:3] == ["exec", "--", "tuist"]] + + def test_generate_keeps_cache_without_authentication(self): + result, calls = self.run_command(["generate", "--no-open"], failure="auth") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(calls, [["generate", "--no-open"]]) + + def test_install_warms_before_generate(self): + result, calls = self.run_command(["install", "--no-open"]) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(calls, [["install"], ["cache", "warm", "--external-only"], ["generate", "--no-open"]]) + + def test_opt_out_does_not_warm(self): + result, calls = self.run_command(["install", "--no-binary-cache", "--no-open"]) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(calls, [["install"], ["generate", "--no-binary-cache", "--no-open"]]) + + def test_ci_does_not_warm(self): + result, calls = self.run_command(["install"], ci=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(calls, [["install"], ["generate"]]) + + def test_failed_install_stops_before_warm(self): + result, calls = self.run_command(["install"], failure="install") + self.assertEqual(result.returncode, 7) + self.assertEqual(calls, [["install"]]) + + def test_failed_warm_stops_before_generate(self): + result, calls = self.run_command(["install"], failure="cache") + self.assertEqual(result.returncode, 7) + self.assertEqual(calls, [["install"], ["cache", "warm", "--external-only"]]) + + def test_cache_warms_external_dependencies(self): + result, calls = self.run_command(["cache"]) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(calls, [["cache", "warm", "--external-only"]]) + + def test_cache_setup_matches_attendance(self): + result, calls = self.run_command(["cache:setup"]) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(calls, [["setup", "cache"]]) + + def test_setup_prepares_cache_before_generate(self): + result, calls = self.run_command(["setup", "--no-open"]) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(calls, [["install"], ["cache", "warm", "--external-only"], ["generate", "--no-open"]]) + + +if __name__ == "__main__": + unittest.main()