using AR; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Chromium; using OpenQA.Selenium.Interactions; using OpenQA.Selenium.Support.UI; using System; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Web.UI.WebControls; using System.Xml.Linq; using UniMarc.SearchModel; using UniMarc.마크; using WebDriverManager; using WebDriverManager.DriverConfigs.Impl; namespace BokBonCheck { public class GwangjuSeoguLibSearcher : ILibrarySearcher { public string AreaCode { get; set; } = string.Empty; public string SiteName { get; protected set; } public virtual string SiteUrl => "https://library.seogu.gwangju.kr/index.9is?contentUid=9be5df897834aa07017868116d3407de"; public bool HttpApiMode { get; set; } = false; public int No { get; set; } private ChromiumDriver _driver; public GwangjuSeoguLibSearcher(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); // 브라우저 배율을 80%로 설정 (브라우저가 표시되는 경우에만) if (showdriver) { try { ((IJavaScriptExecutor)_driver).ExecuteScript("document.body.style.zoom = '0.8';"); Console.WriteLine("브라우저 배율을 80%로 설정했습니다."); } catch (Exception zoomEx) { Console.WriteLine($"브라우저 배율 설정 실패: {zoomEx.Message}"); } } Console.WriteLine("GwangjuSeoguLibSearcher Driver 초기화 완료"); } catch (Exception ex) { Console.WriteLine($"GwangjuSeoguLibSearcher Driver 초기화 실패: {ex.Message}"); throw new InvalidOperationException($"GwangjuSeoguLibSearcher Driver 초기화에 실패했습니다: {ex.Message}", ex); } } } virtual protected bool SelectLibrary(WebDriverWait wait) { try { Console.WriteLine("도서관 선택 과정 시작..."); // 1. 현재 체크된 상태 확인 var allCheckboxes = wait.Until(d => d.FindElements(By.CssSelector("ul.lib_list2 input[type='checkbox']"))); var checkedCheckboxes = allCheckboxes.Where(cb => cb.Selected).ToList(); Console.WriteLine($"현재 체크된 체크박스 개수: {checkedCheckboxes.Count}"); // 2. 지정한 도서관만 정확히 1개 체크되어 있는지 확인 bool isTargetOnlyChecked = false; if (!string.IsNullOrEmpty(AreaCode)) { if (checkedCheckboxes.Count == 1) { var onlyChecked = checkedCheckboxes[0]; var onlyCheckedValue = onlyChecked.GetAttribute("value"); if (onlyCheckedValue == AreaCode) { Console.WriteLine($"✓ {AreaCode} 도서관만 정확히 선택되어 있음 - 완료"); isTargetOnlyChecked = true; } } } // 3. 목표 상태가 아니면 모든 체크박스 해제 후 지정 도서관만 선택 if (!isTargetOnlyChecked) { Console.WriteLine("모든 체크박스 해제 후 지정 도서관만 선택 중..."); // 모든 체크박스를 해제 foreach (var checkbox in allCheckboxes) { try { if (checkbox.Selected) { var checkboxValue = checkbox.GetAttribute("value"); Console.WriteLine($"{checkboxValue} 도서관 체크 해제 중..."); SafeClick(checkbox); Thread.Sleep(100); } } catch (Exception ex) { Console.WriteLine($"체크박스 해제 중 오류: {ex.Message}"); } } // 4. 지정 도서관만 체크 if (!string.IsNullOrEmpty(AreaCode)) { Console.WriteLine($"목표 도서관 '{AreaCode}' 선택 중..."); try { var targetCheckbox = wait.Until(d => d.FindElement(By.CssSelector($"ul.lib_list2 input[type='checkbox'][value='{AreaCode}']"))); if (!targetCheckbox.Selected) { Console.WriteLine($"{AreaCode} 도서관 체크 중..."); SafeClick(targetCheckbox); Thread.Sleep(300); } // 최종 선택 상태 확인 if (targetCheckbox.Selected) { Console.WriteLine($"✓ {AreaCode} 도서관 선택 완료"); } else { Console.WriteLine($"✗ {AreaCode} 도서관 선택 실패"); return false; } } catch (Exception ex) { Console.WriteLine($"목표 도서관 선택 실패: {ex.Message}"); return false; } } else { Console.WriteLine("AreaCode가 비어있음 - 전체 도서관으로 검색"); // 모든 체크박스를 체크 foreach (var checkbox in allCheckboxes) { try { if (!checkbox.Selected) { SafeClick(checkbox); Thread.Sleep(100); } } catch (Exception ex) { Console.WriteLine($"전체 선택 중 오류: {ex.Message}"); } } } } return true; } catch (Exception ex) { Console.WriteLine($"도서관 선택 실패: {ex.Message}"); return false; } } protected void SafeClick(IWebElement element) { // 안정적인 클릭을 위한 여러 방법 시도 try { // 1. 요소가 보이도록 스크롤 후 일반 클릭 ((IJavaScriptExecutor)_driver).ExecuteScript("arguments[0].scrollIntoView(true);", element); Thread.Sleep(300); element.Click(); } catch { try { // 2. JavaScript로 클릭 시도 ((IJavaScriptExecutor)_driver).ExecuteScript("arguments[0].click();", element); } catch { try { // 3. Actions 클래스 사용 var actions = new Actions(_driver); actions.MoveToElement(element).Click().Perform(); } catch { try { // 4. 체크박스의 경우 직접 체크 상태 변경 if (element.TagName.ToLower() == "input" && element.GetAttribute("type") == "checkbox") { ((IJavaScriptExecutor)_driver).ExecuteScript("arguments[0].checked = !arguments[0].checked;", element); } else { // 일반 요소는 강제 클릭 ((IJavaScriptExecutor)_driver).ExecuteScript("arguments[0].dispatchEvent(new Event('click'));", element); } } catch (Exception ex) { Console.WriteLine($"모든 클릭 방법 실패: {ex.Message}"); } } } } } 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)); // 페이지 로드 후 브라우저 배율을 80%로 설정 (브라우저가 표시되는 경우에만) try { if (_driver.Manage().Window.Size.Width > 0) // 브라우저가 표시되는 경우 { ((IJavaScriptExecutor)_driver).ExecuteScript("document.body.style.zoom = '0.8';"); Console.WriteLine("페이지 로드 후 브라우저 배율을 80%로 설정했습니다."); } } catch (Exception zoomEx) { Console.WriteLine($"페이지 배율 설정 실패: {zoomEx.Message}"); } // 도서관 선택 if (SelectLibrary(wait) == false) { result.ErrorMessage = "도서관선택실패"; result.BookCount = -1; result.IsSuccess = false; return result; } // 검색어 입력 try { var searchInput = wait.Until(d => d.FindElement(By.Id("searchWord"))); // 요소가 보이도록 스크롤 ((IJavaScriptExecutor)_driver).ExecuteScript("arguments[0].scrollIntoView(true);", searchInput); Thread.Sleep(300); // 기존 값 제거 후 검색어 입력 searchInput.Clear(); searchInput.SendKeys(searchTerm); Console.WriteLine($"검색어 '{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[name='seachBbsBt']"))); SafeClick(searchButton); Console.WriteLine("검색 버튼 클릭 완료"); } catch (Exception ex) { result.ErrorMessage = $"검색버튼클릭실패({ex.Message})"; result.BookCount = -1; result.IsSuccess = false; return result; } // 페이지 변경을 감지하는 메서드 await WaitForPageChange(new WebDriverWait(_driver, TimeSpan.FromSeconds(15))); // 검색 결과 수 추출 (페이징 포함) var (resultCount, ermsg) = await ExtractBookCountWithPaging(_driver, searchTerm); 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 async Task<(int count, string message)> ExtractBookCountWithPaging(IWebDriver driver, string searchTerm) { string errmessage = string.Empty; int totalCount = 0; try { // 첫 번째 페이지에서 테이블 row 수 확인 var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)); // 검색 결과 테이블이 있는지 확인 try { var bookTable = wait.Until(d => d.FindElement(By.CssSelector("table.board_list tbody#bookList"))); var firstPageRows = bookTable.FindElements(By.TagName("tr")); if (firstPageRows.Count == 0) { errmessage = "검색결과없음"; return (0, errmessage); } totalCount += firstPageRows.Count; Console.WriteLine($"첫 번째 페이지 결과: {firstPageRows.Count}건"); } catch { errmessage = "검색결과없음"; return (0, errmessage); } // 페이징이 있는지 확인하고 각 페이지 방문 try { var paginationDiv = driver.FindElement(By.CssSelector("div.pagination")); var pageLinks = paginationDiv.FindElements(By.TagName("a")).Where(a => !a.GetAttribute("class").Contains("select") && // 현재 페이지가 아닌 것만 !string.IsNullOrEmpty(a.Text.Trim()) && int.TryParse(a.Text.Trim(), out int pageNum) // 숫자인 것만 ).ToList(); Console.WriteLine($"추가 페이지 {pageLinks.Count}개 발견"); foreach (var pageLink in pageLinks) { try { var pageNumber = pageLink.Text.Trim(); Console.WriteLine($"{pageNumber} 페이지로 이동 중..."); SafeClick(pageLink); await Task.Delay(2000); // 페이지 로딩 대기 // 새 페이지에서 결과 수 확인 var newBookTable = wait.Until(d => d.FindElement(By.CssSelector("table.board_list tbody#bookList"))); var pageRows = newBookTable.FindElements(By.TagName("tr")); totalCount += pageRows.Count; Console.WriteLine($"{pageNumber} 페이지 결과: {pageRows.Count}건"); } catch (Exception ex) { Console.WriteLine($"페이지 이동 중 오류: {ex.Message}"); } } } catch { Console.WriteLine("페이징이 없거나 페이징 처리 중 오류 발생"); } if (totalCount == 0) { errmessage = "검색결과없음"; return (0, errmessage); } errmessage = $"검색성공({totalCount}권)"; Console.WriteLine($"전체 검색 결과: {totalCount}건"); return (totalCount, errmessage); } catch (Exception ex) { errmessage = ex.Message; return (-1, errmessage); } } // 페이지 변경을 감지하는 메서드 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("board_list") || pageSource.Contains("bookList") || pageSource.Contains("검색결과가 없습니다"); } catch { return false; } }); await Task.Delay(500); } catch (Exception ex) { // 모든 감지 방법이 실패하면 최소한의 대기 시간 적용 await Task.Delay(3000); Console.WriteLine($"페이지 변경 감지 실패: {ex.Message}"); } } } }