Skip to content

혼합 광고·채팅 흐름·Tuist 캐시 개선과 전체 모듈 문서화 - #44

Merged
Roy-wonji merged 6 commits into
developfrom
feat/mixed-feed-ads
Sep 13, 2026
Merged

Roy-wonji merged 6 commits into
developfrom
feat/mixed-feed-ads

Conversation

@Roy-wonji

@Roy-wonji Roy-wonji commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

변경 이유

홈 외 광고 지면에서 카카오 광고와 서버 광고를 함께 제공하고, 실제 표시된 광고만 노출로 집계합니다.

  • 탐색은 첫 카카오 광고 이후 콘텐츠 3개마다 서버 광고를 순환 표시합니다. 큐레이션·프로필의 단일 지면은 두 광고를 번갈아 표시하며 서버 광고가 없으면 카카오 광고를 사용합니다.
  • AdDomain에 광고 계약·UseCase·Repository를 모으고 slot을 enum으로 정의합니다. 노출은 표시 위치와 광고 코드로 중복을 방지하고, 클릭 분석은 광고 공급자를 구분합니다.
  • Makefile 대신 ./make에서 Tuist 설치·캐시 준비·생성을 실행하고, Tuist Xcode 캐시 및 패키지 잠금을 반영합니다.
  • 채팅 코디네이터를 앱 조립 계층으로 이동하고 투표 뒤로가기·스크롤을 보완합니다. 시나리오 시작 노드가 null이면 첫 노드를 사용합니다.
  • README에 전체 42개 모듈의 실제 의존 관계 SVG와 상세 HTML 문서를 연결합니다.

검증

  • Tuist CLI 모의 테스트 9개 통과
  • 광고 배치 경계 102개 및 빈 광고 목록 검증 통과
  • 변경 Swift 소스 구문 검사 통과
  • 42개 모듈 그림 및 README 일치 검사, git diff --check 통과

검증 범위

전체 앱 빌드는 로컬 Xcode의 clang 정보 수집 단계에서 진행되지 않아 확인하지 못했습니다. 실제 기기 광고 동작과 TCA 테스트 실행은 미검증입니다. 전체 도메인 독립 HTML은 일부 데스크톱 너비에서 가로 넘침이 남아 있으며, GitHub README는 별도 SVG를 표시합니다.

Rejected: Dashboard 인증 실패 시 캐시를 강제로 끄는 흐름 | 로컬 캐시 재사용을 방해한다.
탐색은 첫 Kakao 광고 이후 콘텐츠 3개마다 서버 광고를 순환 표시한다.
큐레이션과 마이페이지는 서버 광고와 Kakao를 교대하고 빈 응답에는 Kakao를 표시한다.
42개 모듈의 직접 구현·Interface 의존성을 SVG로 생성하고 README에서 펼쳐 볼 수 있게 한다.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-13T12:13:23.024225Z 072111c New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

새 탐색 목록을 요청할 때 표시 위치별 노출 기록을 비우고 서버 광고 오류를 로그로 남긴다.
채팅 코디네이터를 앱 조립 계층으로 옮기고 투표 화면의 뒤로가기와 스크롤을 보완한다. 시작 노드가 null인 시나리오는 첫 노드로 매핑한다. 현재 Tuist 및 패키지 설정과 다이어그램 검증 자료를 함께 보존한다.
@Roy-wonji Roy-wonji changed the title 홈 외 혼합 광고 노출과 모듈 의존 관계 문서화 혼합 광고·채팅 흐름·Tuist 캐시 개선과 전체 모듈 문서화 Sep 13, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7dee2d1d30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +20 to +23
_ = try await client.send(
AdsService.impressions(body: AdsImpressionsRequest(codes: codes)),
as: String.self
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Decode impression responses as empty payloads

When the impressions endpoint returns a normal no-content success or an envelope whose data is null, requesting String.self makes NetworkClient.unwrap throw dataMissing, because missing payloads are accepted only for PickeEmptyResponse. In Hifi this removes the deduplication marker and can resend an already accepted impression whenever visibility changes, inflating ad metrics; use the request's default PickeEmptyResponse response instead.

Useful? React with 👍 / 👎.

Comment on lines +8 to +10
public enum FeedAdUseCaseDependency: TestDependencyKey {
public static var testValue: FeedAdInterface { MockFeedAdClient() }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make the use-case implementation the dependency key

Replace this standalone FeedAdUseCaseDependency key with FeedAdUseCaseImpl conforming directly to DependencyKey, including explicit live, test, and preview values. The current parallel interface-level key preserves a duplicate repository/use-case registration topology rather than the repository-hidden, implementation-backed use-case dependency required for new domain IO.

AGENTS.md reference: AGENTS.md:L641-L643

Useful? React with 👍 / 👎.

Comment on lines +171 to +173
case let .adVisible(code, itemID):
guard state.ads.contains(where: { $0.code == code }),
state.reportedAdCodesByItemID[itemID, default: []].insert(code).inserted else { return .none }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset impression deduplication for each screen appearance

When the user leaves Explore and later returns, AppMainTabCoordinator retains the existing Hifi route state, while .onAppear fetches the feed and ads again without clearing reportedAdCodesByItemID. If the same battle/ad pair is displayed after returning or navigating back, this insertion returns false and the new exposure is never reported; clear or scope the deduplication set to the current appearance/feed load.

Useful? React with 👍 / 👎.

Rejected: String 응답 강제 | 성공한 집계를 실패로 오인해 중복 전송할 수 있음
@Roy-wonji
Roy-wonji merged commit a5e7773 into develop Sep 13, 2026
6 checks passed
@Roy-wonji
Roy-wonji deleted the feat/mixed-feed-ads branch September 13, 2026 12:09

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

새로운 광고 기능과 Tuist 캐시 개선 사항에 대한 코드 리뷰입니다. 전반적으로 SwiftUI와 TCA 통합은 잘 구현되었으나, AdDomain 모듈의 아키텍처는 Clean Architecture 원칙에 따라 Data 계층과 더욱 명확히 분리해야 할 필요가 있습니다. 또한, TCAFlow 의존성의 브랜치 사용과 Feature/Ad 모듈의 테스트 누락은 안정성과 검증 측면에서 개선이 필요합니다.

public let network: String
public let title: String
public let subtitle: String
public let imageURL: String

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 [P3] Minor

imageURLclickURL 속성은 String 대신 URL 타입으로 선언하는 것이 더 안전하고, 타입 안정성을 높이며, URL 객체에서 제공하는 유용한 기능을 활용할 수 있습니다.

public let subtitle: String
public let imageURL: String
public let ctaText: String
public let clickURL: String

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 [P3] Minor

imageURLclickURL 속성은 String 대신 URL 타입으로 선언하는 것이 더 안전하고, 타입 안정성을 높이며, URL 객체에서 제공하는 유용한 기능을 활용할 수 있습니다.

public static var testValue: FeedAdInterface { MockFeedAdClient() }
}

public enum FeedAdRepositoryDependency: TestDependencyKey {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [P2] Major

FeedAdRepositoryDependencyAdDomainInterface 모듈이 아닌, Data 계층(AdData 또는 AdDataInterface)에 정의되어야 합니다. Domain 계층은 Repository의 구체적인 존재나 인터페이스에 직접 의존해서는 안 되며, 오직 UseCase 인터페이스에만 의존해야 합니다. 현재 FeedAdInterface가 UseCase와 Repository 인터페이스 역할을 동시에 수행하고 있어 모듈 아키텍처의 책임이 불분명합니다.

product: .framework,
settings: .settings(),
dependencies: [
.serviceAssembly,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [P2] Major

AdDomain.serviceAssembly에 의존하는 것은 모듈 계층 구조 위반입니다. Domain 계층은 Data 계층의 추상화된 인터페이스(DataInterface)에만 의존해야 하며, ServiceAssembly와 같이 Network 또는 APIEndpoint에 가까운 하위 계층에 직접 의존해서는 안 됩니다. 이는 DomainData 간의 단방향 의존성 규칙을 위반합니다.

public static var liveValue: FeedAdInterface { FeedAdUseCaseImpl() }
}

extension FeedAdRepositoryDependency: DependencyKey {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [P2] Major

FeedAdRepositoryDependencyliveValue 구현은 AdDomain 모듈이 아닌 Data 계층(AdData 모듈)에 있어야 합니다. AdDomain은 UseCase 구현을 담당하며, Repository 구현에 대한 지식을 가져서는 안 됩니다. 이는 모듈 아키텍처의 책임 분리 원칙을 위반합니다.


struct AdDomainTests {
@Test func 광고_DTO를_도메인으로_매핑한다() {
let dto = FeedAdDTO(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [P2] Major

FeedAdDTO에 대한 테스트는 Data 계층에 DTO가 위치해야 한다는 원칙에 따라 AdDataTests와 같은 Data 계층 테스트 모듈에서 수행되어야 합니다. 현재 AdDomainTests에서 FeedAdDTO를 직접 테스트하는 것은 DTO-to-Entity 매핑이 Data 계층에 머물러야 한다는 모듈 아키텍처 원칙을 위반합니다.

))
}

@Test func 광고_조회_요청을_매핑한다() throws {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [P2] Major

API 요청 매핑 테스트는 APIEndpoint 모듈의 테스트에서 수행되어야 합니다. Domain 계층 테스트에서 API 요청 세부 사항을 직접 확인하는 것은 DomainNetwork 계층의 구현 세부 사항에 의존하게 만드는 모듈 아키텍처 위반입니다.

#expect(request.url?.query?.contains("size=20") == true)
}

@Test func 광고_노출은_codes_JSON을_POST한다() throws {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [P2] Major

API 요청 매핑 테스트는 APIEndpoint 모듈의 테스트에서 수행되어야 합니다. Domain 계층 테스트에서 API 요청 세부 사항을 직접 확인하는 것은 DomainNetwork 계층의 구현 세부 사항에 의존하게 만드는 모듈 아키텍처 위반입니다.

.service(.analytics, .interface),
],
hasTests: true,
hasTests: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [P2] Major

Feature/Ad 모듈은 복잡한 광고 노출 및 클릭 로직, 그리고 서버 광고와 카카오 광고를 번갈아 표시하는 기능을 포함하고 있습니다. 이러한 복잡성을 고려할 때, hasTests: false로 설정하여 테스트를 비활성화하는 것은 코드 변경에 대한 안정성을 보장하기 어렵습니다. Feature 모듈에 대한 테스트를 추가하여 기능의 정확성을 검증해야 합니다.

Comment thread Tuist/Package.swift
.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"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [P2] Major

TCAFlow 패키지 의존성을 branch: "main"으로 지정하는 것은 빌드 안정성을 저해할 수 있습니다. main 브랜치는 예고 없이 변경될 수 있으며, 이는 예기치 않은 빌드 실패나 동작 변경으로 이어질 수 있습니다. 의존성 관리를 위해 특정 버전(exact) 또는 커밋 해시를 사용하는 것이 권장됩니다.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 072111ca02

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


func recordImpression(for ad: FeedAd) {
guard scenePhase == .active, !didRecordImpression else { return }
didRecordImpression = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear the impression guard when reporting fails

When the impression POST fails while the device is offline or the request otherwise never reaches the server, didRecordImpression remains true for the rest of this appearance, so subsequent visibility or foreground callbacks are discarded and the impression is permanently lost. Reset the flag in the failure path, matching HifiFeature's impressionResponse handling, so the Curation and Profile placements can retry when they become visible again.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant