Skip to content

Commit 00a99c7

Browse files
authored
Merge pull request #155 from YAPP-Github/fix/#152-fetch-url-image
Fix/#152 ์ธ์Šคํƒ€๊ทธ๋žจ ์ด๋ฏธ์ง€ ์กฐํšŒ ๋ฌธ์ œ ์ˆ˜์ •
2 parents b502179 + 1375cc9 commit 00a99c7

File tree

30 files changed

+763
-183
lines changed

30 files changed

+763
-183
lines changed

โ€ŽProjects/App/ShareExtension/Sources/ShareRootFeature.swiftโ€Ž

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,6 @@ struct ShareRootFeature {
200200
.ifLet(\.intro, action: \.intro) { IntroFeature() }
201201
.ifLet(\.contentSetting, action: \.contentSetting) { ContentSettingFeature() }
202202
.forEach(\.path, action: \.path)
203-
._printChanges()
204203
}
205204
}
206205

โ€ŽProjects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupClient+LiveKey.swiftโ€Ž

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,14 @@ import SwiftSoup
1212

1313
extension SwiftSoupClient: DependencyKey {
1414
public static let liveValue: Self = {
15+
let provider = SwiftSoupProvider()
16+
1517
return Self(
16-
parseOGTitleAndImage: { url, completion in
17-
guard let html = try? String(contentsOf: url),
18-
let document = try? SwiftSoup.parse(html) else {
19-
await completion()
20-
return (nil, nil)
21-
}
22-
23-
let title = try? document.select("meta[property=og:title]").first()?.attr("content")
24-
let imageURL = try? document.select("meta[property=og:image]").first()?.attr("content")
25-
26-
guard title != nil || imageURL != nil else {
27-
var request = URLRequest(url: url)
28-
request.setValue(
29-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36",
30-
forHTTPHeaderField: "User-Agent"
31-
)
32-
33-
guard let data = try? await URLSession.shared.data(for: request).0,
34-
let html = String(data: data, encoding: .utf8),
35-
let document = try? SwiftSoup.parse(html) else {
36-
return (nil, nil)
37-
}
38-
39-
let title = try? document.select("meta[property=og:title]").first()?.attr("content")
40-
let imageURL = try? document.select("meta[property=og:image]").first()?.attr("content")
41-
42-
await completion()
43-
44-
return (title, imageURL)
45-
}
46-
47-
await completion()
48-
49-
return (title, imageURL)
18+
parseOGTitle: { url in
19+
try await provider.parseOGTitle(url)
20+
},
21+
parseOGImageURL: { url in
22+
try await provider.parseOGImageURL(url)
5023
}
5124
)
5225
}()

โ€ŽProjects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupClient.swiftโ€Ž

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ import DependenciesMacros
1111

1212
@DependencyClient
1313
public struct SwiftSoupClient {
14-
public var parseOGTitleAndImage: @Sendable (
15-
_ url: URL,
16-
_ completion: @Sendable () async -> Void
17-
) async -> (String?, String?) = { _, _ in (nil , nil) }
14+
public var parseOGTitle: @Sendable (
15+
_ url: URL
16+
) async throws -> String? = { _ in nil }
17+
18+
public var parseOGImageURL: @Sendable (
19+
_ url: URL
20+
) async throws -> String? = { _ in nil }
1821
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// SwiftSoupProvider.swift
3+
// CoreKit
4+
//
5+
// Created by ๊น€๋„ํ˜• on 11/17/24.
6+
//
7+
8+
import SwiftUI
9+
import SwiftSoup
10+
11+
final class SwiftSoupProvider {
12+
func parseOGTitle(_ url: URL) async throws -> String? {
13+
try await parseOGMeta(url: url, type: "og:title")
14+
}
15+
16+
func parseOGImageURL(_ url: URL) async throws -> String? {
17+
try await parseOGMeta(url: url, type: "og:image")
18+
}
19+
20+
func parseOGMeta(url: URL, type: String) async throws -> String? {
21+
let html = try String(contentsOf: url)
22+
let document = try SwiftSoup.parse(html)
23+
24+
if let metaData = try document.select("meta[property=\(type)]").first()?.attr("content") {
25+
return metaData
26+
} else {
27+
var request = URLRequest(url: url)
28+
request.setValue(
29+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36",
30+
forHTTPHeaderField: "User-Agent"
31+
)
32+
33+
let (data, _) = try await URLSession.shared.data(for: request)
34+
guard let html = String(data: data, encoding: .utf8) else {
35+
return nil
36+
}
37+
let document = try SwiftSoup.parse(html)
38+
let metaData = try document.select("meta[property=\(type)]").first()?.attr("content")
39+
40+
return metaData
41+
}
42+
}
43+
}

โ€ŽProjects/DSKit/Sources/Components/PokitLinkCard.swiftโ€Ž

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@ public struct PokitLinkCard<Item: PokitLinkCardItem>: View {
1414
private let link: Item
1515
private let action: () -> Void
1616
private let kebabAction: (() -> Void)?
17+
private let fetchMetaData: (() -> Void)?
1718

1819
public init(
1920
link: Item,
2021
action: @escaping () -> Void,
21-
kebabAction: (() -> Void)? = nil
22+
kebabAction: (() -> Void)? = nil,
23+
fetchMetaData: (() -> Void)? = nil
2224
) {
2325
self.link = link
2426
self.action = action
2527
self.kebabAction = kebabAction
28+
self.fetchMetaData = fetchMetaData
2629
}
2730

2831
public var body: some View {
@@ -110,18 +113,15 @@ public struct PokitLinkCard<Item: PokitLinkCardItem>: View {
110113

111114
@MainActor
112115
private func thumbleNail(url: URL) -> some View {
113-
var request = URLRequest(url: url)
114-
request.setValue(
115-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36",
116-
forHTTPHeaderField: "User-Agent"
117-
)
118-
119-
return LazyImage(request: .init(urlRequest: request)) { phase in
116+
LazyImage(url: url) { phase in
120117
Group {
121118
if let image = phase.image {
122119
image
123120
.resizable()
124121
.aspectRatio(contentMode: .fill)
122+
} else if phase.error != nil {
123+
placeholder
124+
.onAppear { fetchMetaData?() }
125125
} else {
126126
placeholder
127127
}

โ€ŽProjects/DSKit/Sources/Components/PokitLinkPreview.swiftโ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public struct PokitLinkPreview: View {
4141
if let image = phase.image {
4242
image
4343
.resizable()
44+
.aspectRatio(contentMode: .fill)
4445
} else {
4546
PokitSpinner()
4647
.foregroundStyle(.pokit(.icon(.brand)))
@@ -56,6 +57,7 @@ public struct PokitLinkPreview: View {
5657
Spacer()
5758
}
5859
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
60+
.clipped()
5961
.background {
6062
RoundedRectangle(cornerRadius: 12, style: .continuous)
6163
.fill(.pokit(.bg(.base)))

โ€ŽProjects/Domain/Sources/Base/BaseContentItem.swiftโ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public struct BaseContentItem: Identifiable, Equatable, PokitLinkCardItem, Sorta
1414
public let categoryName: String
1515
public let categoryId: Int
1616
public let title: String
17-
public let thumbNail: String
17+
public var thumbNail: String
1818
public let data: String
1919
public let domain: String
2020
public let createdAt: String

โ€ŽProjects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swiftโ€Ž

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import Foundation
88

99
import ComposableArchitecture
10+
import FeatureContentCard
1011
import Domain
1112
import CoreKit
1213
import DSKit
@@ -51,16 +52,7 @@ public struct CategoryDetailFeature {
5152
}
5253
return identifiedArray
5354
}
54-
var contents: IdentifiedArrayOf<BaseContentItem>? {
55-
guard let contentList = domain.contentList.data else {
56-
return nil
57-
}
58-
var identifiedArray = IdentifiedArrayOf<BaseContentItem>()
59-
contentList.forEach { content in
60-
identifiedArray.append(content)
61-
}
62-
return identifiedArray
63-
}
55+
var contents: IdentifiedArrayOf<ContentCardFeature.State> = []
6456
var kebobSelectedType: PokitDeleteBottomSheet.SheetType?
6557
var selectedContentItem: BaseContentItem?
6658
var shareSheetItem: BaseContentItem? = nil
@@ -73,6 +65,7 @@ public struct CategoryDetailFeature {
7365
var hasNext: Bool {
7466
domain.contentList.hasNext
7567
}
68+
var isLoading: Bool = true
7669

7770
public init(category: BaseCategoryItem) {
7871
self.domain = .init(categpry: category)
@@ -86,6 +79,7 @@ public struct CategoryDetailFeature {
8679
case async(AsyncAction)
8780
case scope(ScopeAction)
8881
case delegate(DelegateAction)
82+
case contents(IdentifiedActionOf<ContentCardFeature>)
8983

9084
@CasePathable
9185
public enum View: BindableAction, Equatable {
@@ -121,10 +115,11 @@ public struct CategoryDetailFeature {
121115
case ํด๋ฆฝ๋ณด๋“œ_๊ฐ์ง€
122116
}
123117

124-
public enum ScopeAction: Equatable {
118+
public enum ScopeAction {
125119
case categoryBottomSheet(PokitBottomSheet.Delegate)
126120
case categoryDeleteBottomSheet(PokitDeleteBottomSheet.Delegate)
127121
case filterBottomSheet(CategoryFilterSheet.Delegate)
122+
case contents(IdentifiedActionOf<ContentCardFeature>)
128123
}
129124

130125
public enum DelegateAction: Equatable {
@@ -163,13 +158,19 @@ public struct CategoryDetailFeature {
163158
/// - Delegate
164159
case .delegate(let delegateAction):
165160
return handleDelegateAction(delegateAction, state: &state)
161+
162+
case .contents(let contentsAction):
163+
return .send(.scope(.contents(contentsAction)))
166164
}
167165
}
168166

169167
/// - Reducer body
170168
public var body: some ReducerOf<Self> {
171169
BindingReducer(action: \.view)
172170
Reduce(self.core)
171+
.forEach(\.contents, action: \.contents) {
172+
ContentCardFeature()
173+
}
173174
}
174175
}
175176
//MARK: - FeatureAction Effect
@@ -191,7 +192,7 @@ private extension CategoryDetailFeature {
191192
case .์นดํ…Œ๊ณ ๋ฆฌ_์„ ํƒํ–ˆ์„๋•Œ(let item):
192193
state.domain.category = item
193194
return .run { send in
194-
await send(.inner(.pagenation_์ดˆ๊ธฐํ™”))
195+
await send(.inner(.pagenation_์ดˆ๊ธฐํ™”), animation: .pokitDissolve)
195196
await send(.async(.์นดํ…Œ๊ณ ๋ฆฌ_๋‚ด_์ปจํ…์ธ _๋ชฉ๋ก_์กฐํšŒ_API))
196197
await send(.inner(.์นดํ…Œ๊ณ ๋ฆฌ_์„ ํƒ_์‹œํŠธ_ํ™œ์„ฑํ™”(false)))
197198
}
@@ -248,10 +249,17 @@ private extension CategoryDetailFeature {
248249

249250
case .์นดํ…Œ๊ณ ๋ฆฌ_๋‚ด_์ปจํ…์ธ _๋ชฉ๋ก_์กฐํšŒ_API_๋ฐ˜์˜(let contentList):
250251
state.domain.contentList = contentList
252+
253+
var identifiedArray = IdentifiedArrayOf<ContentCardFeature.State>()
254+
contentList.data?.forEach { identifiedArray.append(.init(content: $0)) }
255+
state.contents = identifiedArray
256+
257+
state.isLoading = false
251258
return .none
252259

253260
case let .์ปจํ…์ธ _์‚ญ์ œ_API_๋ฐ˜์˜(id):
254261
state.domain.contentList.data?.removeAll { $0.id == id }
262+
state.contents.removeAll { $0.content.id == id }
255263
state.domain.category.contentCount -= 1
256264
state.selectedContentItem = nil
257265
state.isPokitDeleteSheetPresented = false
@@ -264,11 +272,15 @@ private extension CategoryDetailFeature {
264272

265273
state.domain.contentList = contentList
266274
state.domain.contentList.data = list + newList
275+
newList.forEach { state.contents.append(.init(content: $0)) }
276+
267277
return .none
268278

269279
case .pagenation_์ดˆ๊ธฐํ™”:
270280
state.domain.pageable.page = 0
271281
state.domain.contentList.data = nil
282+
state.isLoading = true
283+
state.contents.removeAll()
272284
return .none
273285
}
274286
}
@@ -459,6 +471,15 @@ private extension CategoryDetailFeature {
459471
.send(.async(.์นดํ…Œ๊ณ ๋ฆฌ_๋‚ด_์ปจํ…์ธ _๋ชฉ๋ก_์กฐํšŒ_API))
460472
)
461473
}
474+
475+
case let .contents(.element(id: _, action: .delegate(.์ปจํ…์ธ _ํ•ญ๋ชฉ_๋ˆŒ๋ €์„๋•Œ(content)))):
476+
return .send(.delegate(.contentItemTapped(content)))
477+
case let .contents(.element(id: _, action: .delegate(.์ปจํ…์ธ _ํ•ญ๋ชฉ_์ผ€๋ฐฅ_๋ฒ„ํŠผ_๋ˆŒ๋ €์„๋•Œ(content)))):
478+
state.kebobSelectedType = .๋งํฌ์‚ญ์ œ
479+
state.selectedContentItem = content
480+
return .send(.inner(.์นดํ…Œ๊ณ ๋ฆฌ_์‹œํŠธ_ํ™œ์„ฑํ™”(true)))
481+
case .contents:
482+
return .none
462483
}
463484
}
464485

โ€ŽProjects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swiftโ€Ž

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import SwiftUI
88

99
import ComposableArchitecture
10+
import FeatureContentCard
1011
import Domain
1112
import DSKit
1213
import Util
@@ -136,8 +137,8 @@ private extension CategoryDetailView {
136137

137138
var contentScrollView: some View {
138139
Group {
139-
if let contents = store.contents {
140-
if contents.isEmpty {
140+
if !store.isLoading {
141+
if store.contents.isEmpty {
141142
VStack {
142143
PokitCaution(
143144
image: .empty,
@@ -151,17 +152,17 @@ private extension CategoryDetailView {
151152
} else {
152153
ScrollView(showsIndicators: false) {
153154
LazyVStack(spacing: 0) {
154-
ForEach(contents) { content in
155-
let isFirst = content == contents.first
156-
let isLast = content == contents.last
155+
ForEach(
156+
store.scope(state: \.contents, action: \.contents)
157+
) { store in
158+
let isFirst = store.state.id == self.store.contents.first?.id
159+
let isLast = store.state.id == self.store.contents.last?.id
157160

158-
PokitLinkCard(
159-
link: content,
160-
action: { send(.์ปจํ…์ธ _ํ•ญ๋ชฉ_๋ˆŒ๋ €์„๋•Œ(content)) },
161-
kebabAction: { send(.์นดํ…Œ๊ณ ๋ฆฌ_์ผ€๋ฐฅ_๋ฒ„ํŠผ_๋ˆŒ๋ €์„๋•Œ(.๋งํฌ์‚ญ์ œ, selectedItem: content)) }
161+
ContentCardView(
162+
store: store,
163+
isFirst: isFirst,
164+
isLast: isLast
162165
)
163-
.divider(isFirst: isFirst, isLast: isLast)
164-
.pokitScrollTransition(.opacity)
165166
}
166167

167168
if store.hasNext {

0 commit comments

Comments
ย (0)