From f6ddfb72433a822fe5fe50caa96ffdac4bd35ba2 Mon Sep 17 00:00:00 2001 From: chiDT Date: Sun, 29 Jun 2025 22:04:04 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B4=91=EC=A3=BC=EC=8B=9C=EA=B5=90=EC=9C=A1?= =?UTF-8?q?=EC=B2=AD=ED=86=B5=ED=95=A9=EB=8F=84=EC=84=9C=EA=B4=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BokBonCheck/BokBonCheck.csproj | 2 + BokBonCheck/BookSearchService.cs | 16 +- BokBonCheck/ILibrarySearcher.cs | 11 + BokBonCheck/KwangjuCityLibrarySearcher.cs | 267 ++++++++++++++++++++++ BokBonCheck/NamguLibrarySearcher.cs | 205 ++++++++++++++++- 5 files changed, 487 insertions(+), 14 deletions(-) create mode 100644 BokBonCheck/ILibrarySearcher.cs create mode 100644 BokBonCheck/KwangjuCityLibrarySearcher.cs diff --git a/BokBonCheck/BokBonCheck.csproj b/BokBonCheck/BokBonCheck.csproj index 8ae727b..e3172b0 100644 --- a/BokBonCheck/BokBonCheck.csproj +++ b/BokBonCheck/BokBonCheck.csproj @@ -57,6 +57,8 @@ Form + + Form diff --git a/BokBonCheck/BookSearchService.cs b/BokBonCheck/BookSearchService.cs index ce6d99f..61a078d 100644 --- a/BokBonCheck/BookSearchService.cs +++ b/BokBonCheck/BookSearchService.cs @@ -23,8 +23,18 @@ namespace BokBonCheck { _searchers = new List { - new NamguLibrarySearcher() - // 나중에 다른 도서관 검색기를 여기에 추가할 수 있습니다 + new KwangjuCityLibrarySearcher_central(), + new KwangjuCityLibrarySearcher_choisangjun(), + new KwangjuCityLibrarySearcher_dagachi(), + new KwangjuCityLibrarySearcher_Indi(), + new KwangjuCityLibrarySearcher_Student(), + new KwangjuCityLibrarySearcher_Kumho(), + + new NamguLibrarySearcher_Children(), + new NamguLibrarySearcher_Hyocheon(), + new NamguLibrarySearcher_Munhwa(), + new NamguLibrarySearcher_Purungil(), + new NamguLibrarySearcher_Smart(), }; } @@ -68,4 +78,4 @@ namespace BokBonCheck _searchers.Clear(); } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/BokBonCheck/ILibrarySearcher.cs b/BokBonCheck/ILibrarySearcher.cs new file mode 100644 index 0000000..cbaf145 --- /dev/null +++ b/BokBonCheck/ILibrarySearcher.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace BokBonCheck +{ + public interface ILibrarySearcher + { + string SiteName { get; } + string SiteUrl { get; } + Task SearchAsync(string searchTerm); + } +} \ No newline at end of file diff --git a/BokBonCheck/KwangjuCityLibrarySearcher.cs b/BokBonCheck/KwangjuCityLibrarySearcher.cs new file mode 100644 index 0000000..4b78f9a --- /dev/null +++ b/BokBonCheck/KwangjuCityLibrarySearcher.cs @@ -0,0 +1,267 @@ +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; + +namespace BokBonCheck +{ + public class KwangjuCityLibrarySearcher_choisangjun : KwangjuCityLibrarySearcher + { + public KwangjuCityLibrarySearcher_choisangjun() + { + SelectorValue = "MF"; + SiteName = "광주시교육청통합도서관(분관최상준도서관)"; + } + } + public class KwangjuCityLibrarySearcher_dagachi : KwangjuCityLibrarySearcher + { + public KwangjuCityLibrarySearcher_dagachi() + { + SelectorValue = "ME"; + SiteName = "광주시교육청통합도서관(송정다가치문화도서관)"; + } + } + public class KwangjuCityLibrarySearcher_central : KwangjuCityLibrarySearcher + { + public KwangjuCityLibrarySearcher_central() + { + SelectorValue = "MD"; + SiteName = "광주시교육청통합도서관(중앙도서관)"; + } + } + public class KwangjuCityLibrarySearcher_Student : KwangjuCityLibrarySearcher + { + public KwangjuCityLibrarySearcher_Student() + { + SelectorValue = "MC"; + SiteName = "광주시교육청통합도서관(학생교육문화회관)"; + } + } + public class KwangjuCityLibrarySearcher_Kumho : KwangjuCityLibrarySearcher + { + public KwangjuCityLibrarySearcher_Kumho() + { + SelectorValue = "MB"; + SiteName = "광주시교육청통합도서관(금호평생교육관)"; + } + } + public class KwangjuCityLibrarySearcher_Indi : KwangjuCityLibrarySearcher + { + public KwangjuCityLibrarySearcher_Indi() + { + SelectorValue = "MA"; + SiteName = "광주시교육청통합도서관(학생독립운동기념회관)"; + } + } + public abstract class KwangjuCityLibrarySearcher : ILibrarySearcher + { + public string SiteName { get; protected set; } = "광주시교육청통합도서관"; + public string SiteUrl => "https://lib.gen.go.kr/main/site/search/bookSearch.do#simple"; + + protected string SelectorValue = ""; + + + virtual protected void SelectLibrary(WebDriverWait wait) + { + try + { + // 콤보박스(select) 요소 찾기 + var selectElement = wait.Until(d => d.FindElement(By.CssSelector("#manage_code"))); + var select = new OpenQA.Selenium.Support.UI.SelectElement(selectElement); + + // value가 "MA"인 옵션 선택 + select.SelectByValue(SelectorValue); + } + catch + { + // 예외 처리 (필요시 로깅 등) + } + } + + public async Task SearchAsync(string searchTerm) + { + var result = new BookSearchResult + { + SiteName = SiteName, + SearchTerm = searchTerm, + SearchTime = DateTime.Now + }; + + IWebDriver driver = null; + ChromeDriverService service = null; + + try + { + // ChromeDriverManager에서 설정한 드라이버 경로 사용 + var driverPath = ChromeDriverManager.GetDriverPath(); + if (string.IsNullOrEmpty(driverPath) || !File.Exists(driverPath)) + { + // 드라이버가 없으면 다시 설정 + driverPath = await ChromeDriverManager.SetupChromeDriverAsync(); + } + + // ChromeDriver 서비스 설정 + service = ChromeDriverService.CreateDefaultService(Path.GetDirectoryName(driverPath), Path.GetFileName(driverPath)); + service.HideCommandPromptWindow = true; + + var options = new ChromeOptions(); + options.AddArgument("--headless"); + options.AddArgument("--no-sandbox"); + options.AddArgument("--disable-dev-shm-usage"); + options.AddArgument("--disable-gpu"); + options.AddArgument("--window-size=1920,1080"); + options.AddArgument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"); + options.AddArgument("--disable-blink-features=AutomationControlled"); + options.AddExcludedArgument("enable-automation"); + options.AddAdditionalOption("useAutomationExtension", false); + + // 명시적으로 서비스와 옵션을 사용하여 드라이버 생성 + driver = new ChromeDriver(service, options); + + // 자동화 감지 방지 + ((IJavaScriptExecutor)driver).ExecuteScript("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"); + + driver.Navigate().GoToUrl(SiteUrl); + + // 페이지 로딩 대기 + var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(15)); + + //대상도서관 선택 + SelectLibrary(wait); + + // 검색창 찾기 + IWebElement searchBox = null; + try + { + // 여러 가능한 선택자 시도 + var selectors = new[] + { + "input[id='search_txt']", + "input[type='text']", + }; + + foreach (var selector in selectors) + { + try + { + searchBox = wait.Until(d => + { + var el = d.FindElement(By.CssSelector(selector)); + return (el != null && el.Displayed && el.Enabled) ? el : null; + }); + break; + } + catch + { + continue; + } + } + + if (searchBox == null) + { + throw new Exception("검색창을 찾을 수 없습니다."); + } + } + catch (Exception ex) + { + throw new Exception($"검색창 찾기 실패: {ex.Message}"); + } + + // 혹시 readonly라면 속성 제거 + ((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].removeAttribute('readonly')", searchBox); + + // 입력 전 clear + searchBox.Clear(); + searchBox.SendKeys(searchTerm); + + // 검색 버튼 클릭 + IWebElement searchButton = null; + try + { + var buttonSelectors = new[] + { + "input[type='submit']", + }; + //#frm_sch > fieldset > div > div.search_style > div > input + 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 Task.Delay(3000); + + // 검색 결과 수 추출 + var resultCount = ExtractBookCount(driver); + + result.BookCount = resultCount; + result.IsSuccess = true; + } + catch (Exception ex) + { + result.IsSuccess = false; + result.ErrorMessage = ex.Message; + result.BookCount = 0; + } + finally + { + driver?.Quit(); + driver?.Dispose(); + service?.Dispose(); + } + + return result; + } + + private int ExtractBookCount(IWebDriver driver) + { + try + { + // div.search-result 내부의 span에서 '전체 N' 텍스트 추출 + var resultDiv = driver.FindElement(By.CssSelector("div.ndls_result")); + 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) + { + return int.Parse(match.Groups[1].Value); + } + return 0; + } + catch + { + return 0; + } + } + } + +} \ No newline at end of file diff --git a/BokBonCheck/NamguLibrarySearcher.cs b/BokBonCheck/NamguLibrarySearcher.cs index 33e9cef..261aeb7 100644 --- a/BokBonCheck/NamguLibrarySearcher.cs +++ b/BokBonCheck/NamguLibrarySearcher.cs @@ -10,11 +10,198 @@ using System.IO; namespace BokBonCheck { + public class NamguLibrarySearcher_Munhwa : NamguLibrarySearcher + { + public NamguLibrarySearcher_Munhwa() + { + SiteName = "남구통합도서관(문화정보도서관)"; // 문화관 검색기 + } + + protected override void SelectLibrary(WebDriverWait wait) + { + IWebElement searchBox = null; + try + { + var selector = "#clickAll"; + searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); + if (searchBox != null && searchBox.Selected==true) + { + searchBox.Click(); // 전체체크박스가 체크되어 있으면 클릭해서 해제 + } + else return; + + selector = "#libMA"; + searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); + if (searchBox != null && searchBox.Selected==false) + { + searchBox.Click(); // 문화관 체크박스 선택 + } + } + catch + { + + } + } + } + public class NamguLibrarySearcher_Purungil : NamguLibrarySearcher + { + public NamguLibrarySearcher_Purungil() + { + SiteName = "남구통합도서관(푸른길도서관)"; + } + + protected override void SelectLibrary(WebDriverWait wait) + { + IWebElement searchBox = null; + try + { + var selector = "#clickAll"; + searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); + if (searchBox != null && searchBox.Selected == true) + { + searchBox.Click(); // 전체체크박스가 체크되어 있으면 클릭해서 해제 + } + else return; + + selector = "#libMB"; + searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); + if (searchBox != null && searchBox.Selected == false) + { + searchBox.Click(); // 체크박스 선택 + } + } + catch + { + + } + } + } + + public class NamguLibrarySearcher_Children : NamguLibrarySearcher + { + public NamguLibrarySearcher_Children() + { + SiteName = "남구통합도서관(청소년도서관)"; + } + + protected override void SelectLibrary(WebDriverWait wait) + { + IWebElement searchBox = null; + try + { + var selector = "#clickAll"; + searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); + if (searchBox != null && searchBox.Selected == true) + { + searchBox.Click(); // 전체체크박스가 체크되어 있으면 클릭해서 해제 + } + else return; + + selector = "#libMC"; + searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); + if (searchBox != null && searchBox.Selected == false) + { + searchBox.Click(); // 체크박스 선택 + } + } + catch + { + + } + } + } + + public class NamguLibrarySearcher_Hyocheon : NamguLibrarySearcher + { + public NamguLibrarySearcher_Hyocheon() + { + SiteName = "남구통합도서관(효천어울림도서관)"; + } + + protected override void SelectLibrary(WebDriverWait wait) + { + IWebElement searchBox = null; + try + { + var selector = "#clickAll"; + searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); + if (searchBox != null && searchBox.Selected == true) + { + searchBox.Click(); // 전체체크박스가 체크되어 있으면 클릭해서 해제 + } + else return; + + selector = "#libSW"; + searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); + if (searchBox != null && searchBox.Selected == false) + { + searchBox.Click(); // 체크박스 선택 + } + } + catch + { + + } + } + } + public class NamguLibrarySearcher_Smart : NamguLibrarySearcher + { + public NamguLibrarySearcher_Smart() + { + SiteName = "남구통합도서관(스마트도서관)"; + } + + protected override void SelectLibrary(WebDriverWait wait) + { + IWebElement searchBox = null; + try + { + var selector = "#clickAll"; + searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); + if (searchBox != null && searchBox.Selected == true) + { + searchBox.Click(); // 전체체크박스가 체크되어 있으면 클릭해서 해제 + } + else return; + + selector = "#libSQ"; + searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); + if (searchBox != null && searchBox.Selected == false) + { + searchBox.Click(); // 체크박스 선택 + } + } + catch + { + + } + } + } + public class NamguLibrarySearcher : ILibrarySearcher { - public string SiteName => "남구통합도서관"; + public string SiteName { get; protected set; } = "남구통합도서관(전체)"; public string SiteUrl => "https://lib.namgu.gwangju.kr/main/bookSearch"; + virtual protected void SelectLibrary(WebDriverWait wait) + { + IWebElement searchBox = null; + try + { + var selector = "#clickAll"; + searchBox = wait.Until(d => d.FindElement(By.CssSelector(selector))); + if (searchBox != null && searchBox.Selected == false) + { + searchBox.Click(); // 전체체크박스가 해제되어 있으면 클릭해서 선택 + } + else return; + } + catch + { + + } + } + public async Task SearchAsync(string searchTerm) { var result = new BookSearchResult @@ -54,15 +241,18 @@ namespace BokBonCheck // 명시적으로 서비스와 옵션을 사용하여 드라이버 생성 driver = new ChromeDriver(service, options); - + // 자동화 감지 방지 ((IJavaScriptExecutor)driver).ExecuteScript("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"); - + driver.Navigate().GoToUrl(SiteUrl); // 페이지 로딩 대기 var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(15)); + //대상도서관 선택 + SelectLibrary(wait); + // 검색창 찾기 (남구통합도서관 사이트의 특정 선택자 사용) IWebElement searchBox = null; try @@ -193,11 +383,4 @@ namespace BokBonCheck } } } - - public interface ILibrarySearcher - { - string SiteName { get; } - string SiteUrl { get; } - Task SearchAsync(string searchTerm); - } -} \ No newline at end of file +} \ No newline at end of file