using System; using System.Threading.Tasks; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Support.UI; using System.Text.RegularExpressions; using WebDriverManager; using WebDriverManager.DriverConfigs.Impl; using System.IO; using System.Runtime.InteropServices; using System.Threading; using UniMarc.마크; using OpenQA.Selenium.Chromium; using UniMarc.SearchModel; using System.Runtime.CompilerServices; using AR; namespace BokBonCheck { public class GoheungLibSearcher : ILibrarySearcher { public string AreaCode { get; set; } = string.Empty; public string SiteName { get; protected set; } public string SiteUrl => "https://www.ghlib.go.kr/BookSearch/detail"; public bool HttpApiMode { get; set; } = false; public int No { get; set; } private ChromiumDriver _driver; public GoheungLibSearcher(int no, string areaCode, string areaName) { this.No = no; this.AreaCode = areaCode; this.SiteName = $"고흥군립({areaName})"; } public void StopDriver() { if (_driver != null) { _driver.Quit(); _driver.Dispose(); _driver = null; } } public async Task StartDriver(bool showdriver = false) { if (_driver == null) { try { if (SeleniumHelper.IsReady == false) await SeleniumHelper.Download(); _driver = await SeleniumHelper.CreateDriver(ShowBrowser: showdriver); Console.WriteLine("GoheungLibSearcher Driver 초기화 완료"); } catch (Exception ex) { Console.WriteLine($"GoheungLibSearcher Driver 초기화 실패: {ex.Message}"); throw new InvalidOperationException($"GoheungLibSearcher Driver 초기화에 실패했습니다: {ex.Message}", ex); } } } virtual protected bool SelectLibrary(WebDriverWait wait) { try { // 전체 선택인 경우 if (string.IsNullOrEmpty(AreaCode) || AreaCode == "ALL") { var allRadio = wait.Until(d => d.FindElement(By.Id("libALL"))); if (!allRadio.Selected) { SafeClick(allRadio); Thread.Sleep(300); Console.WriteLine("전체도서관 선택됨"); } else { Console.WriteLine("전체도서관이 이미 선택되어 있음"); } return true; } // 특정 도서관 선택인 경우 var targetRadioId = $"lib{AreaCode}"; var targetRadio = wait.Until(d => d.FindElement(By.Id(targetRadioId))); if (targetRadio == null) { Console.WriteLine($"도서관 라디오 버튼을 찾을 수 없습니다: {targetRadioId}"); return false; } // 이미 선택되어 있지 않으면 선택 if (!targetRadio.Selected) { SafeClick(targetRadio); Thread.Sleep(300); Console.WriteLine($"{AreaCode} 도서관으로 변경됨"); } else { Console.WriteLine($"{AreaCode} 도서관이 이미 선택되어 있음"); } return true; } catch (Exception ex) { Console.WriteLine($"도서관 선택 실패: {ex.Message}"); return false; } } protected void SafeClick(IWebElement element) { // 안정적인 클릭을 위한 여러 방법 시도 try { // 1. JavaScript로 클릭 시도 var driver = ((IWrapsDriver)element).WrappedDriver; ((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].click();", element); } catch { try { // 2. 요소가 보이도록 스크롤 후 클릭 var driver = ((IWrapsDriver)element).WrappedDriver; ((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].scrollIntoView(true);", element); Thread.Sleep(500); element.Click(); } catch { try { // 3. Actions 클래스 사용 var driver = ((IWrapsDriver)element).WrappedDriver; var actions = new OpenQA.Selenium.Interactions.Actions(driver); actions.MoveToElement(element).Click().Perform(); } catch { // 4. 마지막 방법: JavaScript로 직접 체크 var driver = ((IWrapsDriver)element).WrappedDriver; ((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].checked = true;", element); } } } } public async Task SearchAsync(string searchTerm) { var result = new BookSearchResult { SiteName = SiteName, SearchTerm = searchTerm, SearchTime = DateTime.Now }; try { // 드라이버가 없으면 자동으로 시작 if (_driver == null) { await StartDriver(); } var cururl = _driver.Url; if (cururl.Equals(SiteUrl) == false) _driver.Navigate().GoToUrl(SiteUrl); // 페이지 로딩 대기 var wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(15)); // 도서관 선택 if (SelectLibrary(wait) == false) { result.ErrorMessage = "도서관선택실패"; result.BookCount = -1; result.IsSuccess = false; return result; } // 도서명 입력 try { var titleInput = wait.Until(d => d.FindElement(By.Id("queryTitle"))); titleInput.Clear(); titleInput.SendKeys(searchTerm); } catch (Exception ex) { result.ErrorMessage = $"도서명입력실패({ex.Message})"; result.BookCount = -1; result.IsSuccess = false; return result; } // 검색 버튼 클릭 try { var searchButton = wait.Until(d => d.FindElement(By.CssSelector("button[type='submit']"))); searchButton.Click(); } catch (Exception ex) { result.ErrorMessage = $"검색버튼클릭실패({ex.Message})"; result.BookCount = -1; result.IsSuccess = false; return result; } // 페이지 변경을 감지하는 메서드 await WaitForPageChange(new WebDriverWait(_driver, TimeSpan.FromSeconds(15))); var htmlContent = _driver.PageSource; var resultCount = ExtractBookCount(htmlContent, out string ermsg, out string resultHtml); result.Resulthtml = resultHtml; if (resultCount == -1) { result.BookCount = 0; result.IsSuccess = false; result.ErrorMessage = ermsg; } else { result.BookCount = resultCount; result.IsSuccess = true; result.ErrorMessage = ermsg; } } catch (Exception ex) { result.IsSuccess = false; result.ErrorMessage = ex.Message; result.BookCount = 0; } return result; } private int ExtractBookCount(string htmlContent, out string errmessage, out string resulthtml) { errmessage = string.Empty; resulthtml = string.Empty; try { // 검색 결과가 없다는 메시지 확인 if (htmlContent.Contains("검색결과가 없습니다") || htmlContent.Contains("검색된 자료가 없습니다") || htmlContent.Contains("자료가 없습니다") || htmlContent.Contains("전체 0 건")) { errmessage = "검색결과없음"; resulthtml = "검색결과가 없습니다"; return 0; } // HTML에서 "전체 N 건" 패턴 찾기 var htmlPatterns = new[] { @"전체\s*(\d+)\s*건", @"전체\s+(\d+)\s+건", @"총\s*(\d+)\s*권", @"총\s*(\d+)\s*건" }; foreach (var pattern in htmlPatterns) { var match = Regex.Match(htmlContent, pattern, RegexOptions.IgnoreCase); if (match.Success) { if (int.TryParse(match.Groups[1].Value, out int count)) { // 매칭된 부분과 상위 태그 추출하여 resulthtml에 저장 resulthtml = ExtractResultContext(htmlContent, match); if (count == 0) { errmessage = "검색결과없음"; return 0; } errmessage = $"검색성공({count}권)"; return count; } } } errmessage = "결과수량을찾을수없음"; resulthtml = "검색결과 패턴을 찾을 수 없음"; return -1; } catch (Exception ex) { errmessage = ex.Message; resulthtml = "검색결과 패턴을 찾을 수 없음"; return -1; } } /// /// 매칭된 결과와 그 상위 태그를 추출 /// private string ExtractResultContext(string htmlContent, Match match) { try { var matchIndex = match.Index; var matchLength = match.Length; // 매칭된 위치 앞쪽에서 상위 태그 시작 찾기 var startSearchIndex = Math.Max(0, matchIndex - 200); // 매칭 위치 200자 전부터 검색 var searchText = htmlContent.Substring(startSearchIndex, matchIndex - startSearchIndex + matchLength + Math.Min(200, htmlContent.Length - matchIndex - matchLength)); // 상위 태그 패턴들 (div, p, h1-h6, span 등) var tagPatterns = new[] { @"<(div|p|h[1-6]|span|section|article)[^>]*>", @"<[^>]+>" }; string resultContext = match.Value; // 기본값은 매칭된 부분만 foreach (var tagPattern in tagPatterns) { // 매칭된 부분 앞에서 가장 가까운 태그 시작 찾기 var tagMatches = Regex.Matches(searchText, tagPattern, RegexOptions.IgnoreCase); for (int i = tagMatches.Count - 1; i >= 0; i--) { var tagMatch = tagMatches[i]; if (tagMatch.Index < (matchIndex - startSearchIndex)) { // 태그 이름 추출 var tagName = Regex.Match(tagMatch.Value, @"<(\w+)", RegexOptions.IgnoreCase).Groups[1].Value; // 닫는 태그 찾기 var closeTagPattern = $@"]*>"; var closeMatch = Regex.Match(searchText, closeTagPattern, RegexOptions.IgnoreCase); if (closeMatch.Success && closeMatch.Index > (matchIndex - startSearchIndex)) { // 상위 태그와 그 내용을 포함하여 반환 var startIdx = tagMatch.Index; var endIdx = closeMatch.Index + closeMatch.Length; resultContext = searchText.Substring(startIdx, Math.Min(endIdx - startIdx, 500)); // 최대 500자 return resultContext; } } } } // 상위 태그를 찾지 못한 경우, 매칭 전후 50자씩 포함 var contextStart = Math.Max(0, matchIndex - 50); var contextEnd = Math.Min(htmlContent.Length, matchIndex + matchLength + 50); resultContext = htmlContent.Substring(contextStart, contextEnd - contextStart); return resultContext; } catch (Exception ex) { Console.WriteLine($"ExtractResultContext 오류: {ex.Message}"); return match.Value; // 오류 시 매칭된 부분만 반환 } } // 페이지 변경을 감지하는 메서드 public async Task WaitForPageChange(WebDriverWait wait) { try { await Task.Delay(500); // 페이지 로딩 상태 확인 wait.Until(d => { var readyState = ((IJavaScriptExecutor)d).ExecuteScript("return document.readyState"); return readyState.Equals("complete"); }); // 검색 결과 페이지가 로드될 때까지 대기 wait.Until(d => { try { var pageSource = d.PageSource; // 검색 결과나 "검색결과가 없습니다" 메시지가 나타나면 로드 완료 return pageSource.Contains("검색결과") || pageSource.Contains("총") || pageSource.Contains("자료가") || pageSource.Contains("bookList") || pageSource.Contains("searchResult"); } catch { return false; } }); } catch (Exception ex) { // 모든 감지 방법이 실패하면 최소한의 대기 시간 적용 await Task.Delay(3000); Console.WriteLine($"페이지 변경 감지 실패: {ex.Message}"); } } } }