From af2d7f383b786cc183902fa3dd38cc05af179b9a Mon Sep 17 00:00:00 2001 From: SwainYun Date: Wed, 6 May 2026 01:36:23 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[Feat]=20#233=20-=20=EB=AA=A8=EB=85=B8?= =?UTF-8?q?=EB=A7=A8=EC=85=98=20=EB=B8=8C=EB=9E=9C=EB=93=9C=20QR=20?= =?UTF-8?q?=ED=8C=8C=EC=8B=B1=20=EC=A0=84=EB=9E=B5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DefaultQRCodeScanRepository.swift | 1 + .../Implementations/MonomansionStrategy.swift | 120 ++++++++++-------- .../Domain/Sources/Entities/QRCodeBrand.swift | 3 + 3 files changed, 71 insertions(+), 53 deletions(-) diff --git a/Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Repositories/DefaultQRCodeScanRepository.swift b/Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Repositories/DefaultQRCodeScanRepository.swift index 68b8b90b..18763feb 100644 --- a/Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Repositories/DefaultQRCodeScanRepository.swift +++ b/Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Repositories/DefaultQRCodeScanRepository.swift @@ -15,6 +15,7 @@ struct DefaultQRCodeScanRepository: QRCodeScanRepository { PhotograyStrategy(), PhotoSignatureStrategy(), Life4CutStrategy(), + MonomansionStrategy(), PhotoismStrategy() ] diff --git a/Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Strategies/Implementations/MonomansionStrategy.swift b/Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Strategies/Implementations/MonomansionStrategy.swift index dfb83802..90842053 100644 --- a/Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Strategies/Implementations/MonomansionStrategy.swift +++ b/Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Strategies/Implementations/MonomansionStrategy.swift @@ -6,57 +6,71 @@ // import Foundation +import os -// TODO: 모노맨션 브랜드는 나중에 출시 -//struct MonoMansionStrategy: QRCodeParsingStrategy { -// var strategyType: ParsingStrategyType { .htmlCrawling } -// -// func canHandle(host: String) -> Bool { -// PhotoBoothBrand.monoMansion.hostKeywords.contains(host) -// } -// -// func parse(_ url: URL, networkProvider: NetworkProvider) async throws(QRParseError) -> ParsedQRResult { -// // 1. HTML 데이터 요청 -// var request = URLRequest(url: url) -// // 봇 차단 방지용 헤더 -// request.setValue("Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1", forHTTPHeaderField: "User-Agent") -// -// let data: Data -// do { -// // 현재 단계에서는 URLSession 직접 사용 -// (data, _) = try await URLSession.shared.data(for: request) -// } catch { -// guard let urlError = error as? URLError else { throw .parsingFailed } -// switch urlError.code { -// case .notConnectedToInternet, .networkConnectionLost: -// throw .networkError(.networkFail) -// default: -// throw .networkError(.unknownError(urlError)) -// } -// } -// -// // 2. HTML 문자열 변환 -// guard let htmlString = String(data: data, encoding: .utf8) else { -// throw .parsingFailed -// } -// -// // 3. 정규식을 통한 이미지 URL 추출 -// // 제공된 정규식: href\s*=\s*["'](https://[^"']*ncloudstorage\.com[^"']+\.jpg)["'] -// // 그룹 1번(괄호 안의 내용)이 실제 URL입니다. -// let pattern = "href\\s*=\\s*[\"'](https://[^\"']*ncloudstorage\\.com[^\"']+\\.jpg)[\"']" -// -// guard let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive), -// let match = regex.firstMatch(in: htmlString, options: [], range: NSRange(location: 0, length: htmlString.utf16.count)), -// let range = Range(match.range(at: 1), in: htmlString) else { -// throw .parsingFailed -// } -// -// let imageURLString = String(htmlString[range]) -// -// guard let imageURL = URL(string: imageURLString) else { -// throw .urlConstructionFailed -// } -// -// return ParsedQRResult(brand: .monoMansion, originalImageURL: imageURL) -// } -//} +struct MonomansionStrategy: QRCodeParsingStrategy { + var strategyType: ParsingStrategyType { .htmlCrawling } + + func canHandle(host: String) -> Bool { QRCodeBrand.monoMansion.hostKeywords.contains { host.lowercased().contains($0.lowercased()) } } + + func parse(_ url: URL, networkProvider: NetworkProvider) async throws(QRParseError) -> ParsedQRResult { + Logger.data.debug("모노맨션 파싱 시도: \(url.absoluteString)") + + let request = URLRequest(url: url) + let htmlData: Data + + do { + (htmlData, _) = try await URLSession.shared.data(for: request) + } catch { + Logger.network.error("모노맨션 HTML 요청 실패: \(error.localizedDescription)") + if let urlError = error as? URLError, + [.notConnectedToInternet, .networkConnectionLost].contains(urlError.code) { + throw .networkError(.networkFail) + } + throw .fallbackToWebView(url) + } + + guard let htmlString = String(data: htmlData, encoding: .utf8) else { + Logger.domain.warning("모노맨션 HTML 문자열 변환 실패.") + throw .fallbackToWebView(url) + } + + let pattern = #"href\s*=\s*["'](https://[^"']*ncloudstorage\.com[^"']+\.jpg)["']"# + guard let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive), + let match = regex.firstMatch( + in: htmlString, + options: [], + range: NSRange(location: 0, length: htmlString.utf16.count) + ), + let range = Range(match.range(at: 1), in: htmlString) + else { + Logger.domain.warning("모노맨션 이미지 URL 추출 실패. 웹뷰 폴백.") + throw .fallbackToWebView(url) + } + + let imageURLString = String(htmlString[range]) + .replacingOccurrences(of: "&", with: "&") + + guard let imageURL = URL(string: imageURLString) else { + Logger.domain.error("모노맨션 이미지 URL 생성 실패.") + throw .fallbackToWebView(url) + } + + do { + let (data, response) = try await URLSession.shared.data(from: imageURL) + + if let httpResponse = response as? HTTPURLResponse, + (200..<300).contains(httpResponse.statusCode) == false { + Logger.network.warning("모노맨션 이미지 다운로드 실패 (상태코드: \(httpResponse.statusCode)). 웹뷰 폴백.") + throw QRParseError.fallbackToWebView(url) + } + + return ParsedQRResult(brand: .monoMansion, originalImage: data) + } catch let error as QRParseError { + throw error + } catch { + Logger.network.warning("모노맨션 이미지 다운로드 실패: \(error.localizedDescription)") + throw .imageDownloadFailed + } + } +} diff --git a/Neki-iOS/Features/QRCodeScanner/Sources/Domain/Sources/Entities/QRCodeBrand.swift b/Neki-iOS/Features/QRCodeScanner/Sources/Domain/Sources/Entities/QRCodeBrand.swift index 964cbd5b..d7543a68 100644 --- a/Neki-iOS/Features/QRCodeScanner/Sources/Domain/Sources/Entities/QRCodeBrand.swift +++ b/Neki-iOS/Features/QRCodeScanner/Sources/Domain/Sources/Entities/QRCodeBrand.swift @@ -15,6 +15,7 @@ public enum QRCodeBrand: CustomStringConvertible, Sendable { case photoism case photogray case photosignature + case monoMansion case planBStudio case harufilm @@ -24,6 +25,7 @@ public enum QRCodeBrand: CustomStringConvertible, Sendable { case .photoism: ["seobuk.kr"] case .photogray: ["aprd.io", "pgshort.aprd.io"] case .photosignature: ["photoqr3.kr"] + case .monoMansion: ["qr.mono-mansion.com"] case .planBStudio: [] case .harufilm: ["haru4.mx2.co.kr", "haru3.mx2.co.kr", "haru2.mx2.co.kr", "haru1.mx2.co.kr", "haru.mx2.co.kr"] } @@ -35,6 +37,7 @@ public enum QRCodeBrand: CustomStringConvertible, Sendable { case .photoism: return "포토이즘" case .photogray: return "포토그레이" case .photosignature: return "포토시그니처" + case .monoMansion: return "모노맨션" case .planBStudio: return "플랜비스튜디오" case .harufilm: return "하루필름" } From 7950096f5e51c1b201dafeceba07d9b2bbf6cebb Mon Sep 17 00:00:00 2001 From: SwainYun Date: Wed, 6 May 2026 13:47:18 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[Chore]=20#233=20-=20=EB=AA=A8=EB=85=B8?= =?UTF-8?q?=EB=A7=A8=EC=85=98=20=ED=8C=8C=EC=8B=B1=20=EC=A0=84=EB=9E=B5=20?= =?UTF-8?q?=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Strategies/Implementations/MonomansionStrategy.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Strategies/Implementations/MonomansionStrategy.swift b/Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Strategies/Implementations/MonomansionStrategy.swift index 90842053..a52af9d4 100644 --- a/Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Strategies/Implementations/MonomansionStrategy.swift +++ b/Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Strategies/Implementations/MonomansionStrategy.swift @@ -35,7 +35,7 @@ struct MonomansionStrategy: QRCodeParsingStrategy { throw .fallbackToWebView(url) } - let pattern = #"href\s*=\s*["'](https://[^"']*ncloudstorage\.com[^"']+\.jpg)["']"# + let pattern = #"href\s*=\s*["'](https://[^"']*ncloudstorage\.com[^"']+\.jpg(?:\?[^"']*)?)["']"# guard let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive), let match = regex.firstMatch( in: htmlString,