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 OpenQA.Selenium.Chromium; using UniMarc.SearchModel; using System.Runtime.CompilerServices; namespace BokBonCheck { public class NamguLibrarySearcher : ILibrarySearcher { public string AreaCode { get; set; } = string.Empty; public string SiteName { get; protected set; } = "남구통합도서관(전체)"; public string SiteUrl => "https://lib.namgu.gwangju.kr/main/bookSearch"; public bool HttpApiMode { get; set; } = false; public int No { get; set; } private ChromiumDriver _driver; [DllImport("user32.dll")] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); private const int SW_MINIMIZE = 6; public NamguLibrarySearcher(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("NamguLibrarySearcher Driver 초기화 완료"); } catch (Exception ex) { Console.WriteLine($"NamguLibrarySearcher Driver 초기화 실패: {ex.Message}"); throw new InvalidOperationException($"NamguLibrarySearcher Driver 초기화에 실패했습니다: {ex.Message}", ex); } } } virtual protected bool SelectLibrary(WebDriverWait wait) { IWebElement searchBox = null; try { var selector = "#clickAll"; searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); if (searchBox == null) return false; if (searchBox.Selected == true) { SafeClick(searchBox); } selector = AreaCode; searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); if (searchBox == null) return false; if (searchBox.Selected == false) { SafeClick(searchBox); } return true; } catch { return false; } } protected void SafeClick(IWebElement searchBox) { // 안정적인 클릭을 위한 여러 방법 시도 try { // 1. JavaScript로 클릭 시도 var driver = ((IWrapsDriver)searchBox).WrappedDriver; ((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].click();", searchBox); } catch { try { // 2. 요소가 보이도록 스크롤 후 클릭 var driver = ((IWrapsDriver)searchBox).WrappedDriver; ((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].scrollIntoView(true);", searchBox); Thread.Sleep(500); searchBox.Click(); } catch { try { // 3. Actions 클래스 사용 var driver = ((IWrapsDriver)searchBox).WrappedDriver; var actions = new OpenQA.Selenium.Interactions.Actions(driver); actions.MoveToElement(searchBox).Click().Perform(); } catch { // 4. 마지막 방법: JavaScript로 직접 체크 해제 var driver = ((IWrapsDriver)searchBox).WrappedDriver; ((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].checked = false;", searchBox); } } } } public async Task SearchAsync(string searchTerm) { var result = new BookSearchResult { SiteName = SiteName, SearchTerm = searchTerm, SearchTime = DateTime.Now }; try { // 드라이버가 없으면 자동으로 시작 if (_driver == null) { await StartDriver(); } _driver.Navigate().GoToUrl(SiteUrl); // 페이지 로딩 대기 var wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(15)); // 모든 감지 방법이 끝나면 크롬창 최소화 // whale 브라우저가 최소화되어 우선해제 //IntPtr chromeWindow = FindWindow("Chrome_WidgetWin_1", null); //if (chromeWindow != IntPtr.Zero) //{ // ShowWindow(chromeWindow, SW_MINIMIZE); //} //대상도서관 선택 if (SelectLibrary(wait) == false) { result.ErrorMessage = "도서관선택실패"; result.BookCount = -1; result.IsSuccess = false; return result; } // 검색창 찾기 (남구통합도서관 사이트의 특정 선택자 사용) IWebElement searchBox = null; try { // 여러 가능한 선택자 시도 var selectors = new[] { "input[name='query']", "input[id='query']", "input[type='text']", }; foreach (var selector in selectors) { try { searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); break; } catch { continue; } } if (searchBox == null) { throw new Exception("검색창을 찾을 수 없습니다."); } } catch (Exception ex) { throw new Exception($"검색창 찾기 실패: {ex.Message}"); } // 검색어 입력 searchBox.Clear(); searchBox.SendKeys(searchTerm); // 검색 버튼 클릭 IWebElement searchButton = null; try { var buttonSelectors = new[] { "button[type='submit']", "input[type='submit']", ".search-btn", ".btn-search", "button:contains('검색')", "input[value*='검색']", "button[class*='search']", "input[class*='search']" }; foreach (var selector in buttonSelectors) { try { searchButton = _driver.FindElement(By.CssSelector(selector)); break; } catch { continue; } } if (searchButton == null) { // Enter 키로 검색 시도 searchBox.SendKeys(Keys.Enter); } else { searchButton.Click(); } } catch (Exception ex) { // Enter 키로 검색 시도 searchBox.SendKeys(Keys.Enter); } // 페이지 변경을 감지하는 메서드 await WaitForPageChange(new WebDriverWait(_driver, TimeSpan.FromSeconds(15))); // 검색 결과 수 추출 var resultCount = ExtractBookCount(_driver, searchTerm, 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(IWebDriver driver, string searchTerm, out string errmessage, out string resulthtml) { errmessage = string.Empty; resulthtml = string.Empty; try { var htmlContent = driver.PageSource; // div.search-result 내부의 span에서 '전체 N' 텍스트 추출 var resultDiv = driver.FindElement(By.CssSelector("div.search-result")); var bodytext = resultDiv.Text; if (bodytext.Contains("검색결과가 없습니다")) { errmessage = "검색결과없음"; resulthtml = bodytext; return 0; } var searchkey = resultDiv.FindElement(By.XPath("//*[@id=\"sub\"]/section[3]/div/div/div/div/div[2]/div[1]/p/b")); var searchtitle = searchkey.Text; if (searchTerm.Contains(searchtitle) == false) { errmessage = $"검색어불일치({searchtitle}/{searchTerm})"; resulthtml = searchtitle; return -1; } var span = resultDiv.FindElement(By.XPath(".//span[contains(text(),'전체')]")); string text = span.Text; // 예: "전체 5 " var match = System.Text.RegularExpressions.Regex.Match(text, @"전체\s*(\d+)"); if (match.Success) { if (int.TryParse(match.Groups[1].Value, out int vqty) == false) { errmessage = $"수량값오류({match.Groups[1].Value})"; resulthtml = match.Value; return -1; } else { errmessage = $"검색성공({vqty}건)"; resulthtml = ExtractResultContext(htmlContent, match); searchTerm = string.Empty; return vqty; } } else { errmessage = "수량항목없음"; // 매칭된 부분이 없는 경우, 기본적으로 text 부분 추출 시도 try { var dummyMatch = System.Text.RegularExpressions.Regex.Match(text, @"전체.*"); if (dummyMatch.Success) { resulthtml = ExtractResultContext(htmlContent, dummyMatch); } else { resulthtml = htmlContent.Length > 500 ? htmlContent.Substring(0, 500) : htmlContent; } } catch { resulthtml = htmlContent.Length > 500 ? htmlContent.Substring(0, 500) : htmlContent; } return -1; } } catch (Exception ex) { errmessage = ex.Message; resulthtml = "ExtractBookCount 오류: " + ex.Message; 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); // 방법 4: 페이지 로딩 상태 확인 wait.Until(d => { var readyState = ((IJavaScriptExecutor)d).ExecuteScript("return document.readyState"); return readyState.Equals("complete"); }); // 방법 5: 특정 텍스트가 페이지에 나타날 때까지 대기 wait.Until(d => { try { var byclassname = By.ClassName("search-result"); var elm = d.FindElement(byclassname); if (elm == null) { return false; } var pageText = elm.Text; if (pageText.Contains("검색결과가 없습니다")) return true; return pageText.Contains("에 대하여") && pageText.Contains("검색되었습니다"); } catch { return false; } }); } catch (Exception ex) { // 모든 감지 방법이 실패하면 최소한의 대기 시간 적용 await Task.Delay(2000); throw new Exception($"페이지 변경 감지 실패: {ex.Message}"); } } } }