Skip to content

Commit 0565565

Browse files
Merge pull request #189 from YAPP-Github/feat/#188-qr-parsing-webhook
[Feat] QR 스캔 미지원 브랜드, 신속한 파싱 전략 수립을 위한 디스코드 웹훅 설정 #188
2 parents 4c8fd3f + 0719dfd commit 0565565

8 files changed

Lines changed: 117 additions & 7 deletions

File tree

Neki-iOS/APP/Sources/MainTab/MainTabCoordinator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ struct MainTabCoordinator {
153153
}
154154

155155
case .qrScannerPresented:
156-
state.destination = .qrScan(QRCodeScanFeature.State())
156+
state.destination = .qrScan(QRCodeScanFeature.State(user: state.user))
157157
return .none
158158

159159
case .presentPermissionAlert:
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// DiscordWebhookDTO.swift
3+
// Neki-iOS
4+
//
5+
// Created by SwainYun on 3/24/26.
6+
//
7+
8+
import Foundation
9+
10+
public enum DiscordWebhookDTO {
11+
public struct Request: Encodable {
12+
struct Embed: Encodable {
13+
let title: String
14+
let description: String
15+
let color: Int
16+
let fields: [Field]
17+
}
18+
19+
struct Field: Encodable {
20+
let name: String
21+
let value: String
22+
let inline: Bool
23+
}
24+
25+
let embeds: [Embed]
26+
27+
init(unsupportedURL: URL, user: User) {
28+
let host = unsupportedURL.host() ?? "Unknown Host"
29+
let embed = Embed(
30+
title: "🚨 미지원 QR 코드 스캔 감지",
31+
description: "새로운 포토부스 브랜드이거나 파싱에 실패한 URL입니다.",
32+
color: 16711680, // 빨강색 (Hex: FF0000)
33+
fields: [
34+
Field(name: "🍎 Platform", value: "iOS", inline: true),
35+
Field(name: "👤 User", value: "\(user.nickname) (ID: \(user.id))", inline: true),
36+
Field(name: "🌐 Host", value: host, inline: true),
37+
Field(name: "🔗 Full URL", value: unsupportedURL.absoluteString, inline: false),
38+
]
39+
)
40+
41+
self.embeds = [embed]
42+
}
43+
}
44+
}

Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Repositories/DefaultQRCodeScanRepository.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import Foundation
99
import Dependencies
10+
import os
1011

1112
struct DefaultQRCodeScanRepository: QRCodeScanRepository {
1213
private let strategies: [QRCodeParsingStrategy] = [
@@ -19,14 +20,23 @@ struct DefaultQRCodeScanRepository: QRCodeScanRepository {
1920

2021
@Dependency(\.networkProvider) private var networkProvider
2122

22-
func parse(_ url: URL) async throws(QRParseError) -> ParsedQRResult {
23+
func parse(_ url: URL, user: User) async throws(QRParseError) -> ParsedQRResult {
2324
guard let host = url.host() else { throw .invalidURL }
2425

2526
for strategy in strategies {
2627
guard strategy.canHandle(host: host) else { continue }
2728
return try await strategy.parse(url, networkProvider: networkProvider)
2829
}
2930

31+
Task.detached(priority: .background) {
32+
do {
33+
let endpoint = QRCodeScannerEndpoint.notifyUnsupportedBrand(url: url, user: user)
34+
try await networkProvider.requestVoid(endpoint: endpoint)
35+
} catch {
36+
Logger.network.error("미지원 브랜드 디스코드 웹훅 발송 실패: \(error)")
37+
}
38+
}
39+
3040
throw .unsupportedBrand
3141
}
3242
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// QRCodeScannerEndpoint.swift
3+
// Neki-iOS
4+
//
5+
// Created by SwainYun on 3/24/26.
6+
//
7+
8+
import Foundation
9+
import os
10+
11+
public enum QRCodeScannerEndpoint {
12+
case notifyUnsupportedBrand(url: URL, user: User)
13+
}
14+
15+
16+
// MARK: - QRCodeScannerEndpoint + Endpoint
17+
18+
extension QRCodeScannerEndpoint: Endpoint {
19+
public var baseURL: String {
20+
switch self {
21+
case .notifyUnsupportedBrand:
22+
guard let webhookURLString = Bundle.main.infoDictionary?["QR_WEBHOOK_URL"] as? String else {
23+
Logger.data.fault("QR_WEBHOOK_URL을 번들에서 찾을 수 없음.")
24+
return ""
25+
}
26+
return webhookURLString
27+
}
28+
}
29+
30+
public var path: String {
31+
switch self {
32+
case .notifyUnsupportedBrand: return ""
33+
}
34+
}
35+
36+
public var method: HTTPMethodType {
37+
switch self {
38+
case .notifyUnsupportedBrand: return .post
39+
}
40+
}
41+
42+
public var authorizationType: AuthorizationType { return .none }
43+
44+
public var contentType: HTTPContentType { return .json }
45+
46+
public var body: (any Encodable)? {
47+
switch self {
48+
case let .notifyUnsupportedBrand(url, user): return DiscordWebhookDTO.Request(unsupportedURL: url, user: user)
49+
}
50+
}
51+
}

Neki-iOS/Features/QRCodeScanner/Sources/Domain/Sources/Clients/QRCodeScannerClient.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import ComposableArchitecture
1212
struct QRScannerClient {
1313
var checkAuthorizationStatus: @Sendable () -> AVAuthorizationStatus
1414
var requestAccess: @Sendable () async -> Bool
15-
var parse: @Sendable (_ urlString: String) async throws -> ParsedQRResult
15+
var parse: @Sendable (_ urlString: String, User) async throws -> ParsedQRResult
1616
var processImage: @Sendable (_ data: Data) async throws -> [Int]
1717
}
1818

@@ -25,9 +25,9 @@ extension QRScannerClient: DependencyKey {
2525
AVCaptureDevice.authorizationStatus(for: .video)
2626
} requestAccess: {
2727
await AVCaptureDevice.requestAccess(for: .video)
28-
} parse: { urlString in
28+
} parse: { urlString, user in
2929
guard let url = URL(string: urlString) else { throw QRParseError.invalidURL }
30-
return try await qrCodeScanRepository.parse(url)
30+
return try await qrCodeScanRepository.parse(url, user: user)
3131
} processImage: { data in
3232
let processed = await ImageDownsamplingProcessor.process(data: data)
3333
guard let imageData = processed?.data else { throw QRParseError.parsingFailed }

Neki-iOS/Features/QRCodeScanner/Sources/Domain/Sources/Interfaces/QRCodeScanRepository.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public enum QRParseError: Error {
2020
}
2121

2222
public protocol QRCodeScanRepository {
23-
func parse(_ url: URL) async throws(QRParseError) -> ParsedQRResult
23+
func parse(_ url: URL, user: User) async throws(QRParseError) -> ParsedQRResult
2424
}
2525

2626
private enum QRCodeScanRepositoryKey: DependencyKey {

Neki-iOS/Features/QRCodeScanner/Sources/Presentation/Sources/Features/QRCodeScanFeature.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import os
1414
struct QRCodeScanFeature {
1515
@ObservableState
1616
struct State {
17+
let user: User
18+
1719
var isLightOn: Bool = false
1820
var isLoading: Bool = false
1921

@@ -105,11 +107,12 @@ struct QRCodeScanFeature {
105107
// MARK: - Scanning Flow
106108
case .codeScanned(let urlString):
107109
guard state.isLoading == false, state.isCameraActive else { return .none }
110+
let user = state.user
108111
state.isLoading = true
109112
Logger.presentation.debug("QR 스캔 감지: \(urlString)")
110113

111114
return .run { send in
112-
await send(.parseQRResult(Result { try await qrScannerClient.parse(urlString) }))
115+
await send(.parseQRResult(Result { try await qrScannerClient.parse(urlString, user) }))
113116
}
114117

115118
case let .parseQRResult(.success(parsed)):

Neki-iOS/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<key>QR_WEBHOOK_URL</key>
6+
<string>$(QR_WEBHOOK_URL)</string>
57
<key>BASE_URL</key>
68
<string>$(BASE_URL)</string>
79
<key>CFBundleURLTypes</key>

0 commit comments

Comments
 (0)