초기 커밋.

This commit is contained in:
2025-06-28 22:14:18 +09:00
parent 40dc2254d5
commit ab7a315b04
18 changed files with 1936 additions and 0 deletions

25
BokBonCheck.sln Normal file
View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35919.96 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BokBonCheck", "BokBonCheck\BokBonCheck.csproj", "{BFFF0C63-1231-442F-B800-C21760132E4D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BFFF0C63-1231-442F-B800-C21760132E4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BFFF0C63-1231-442F-B800-C21760132E4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFFF0C63-1231-442F-B800-C21760132E4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFFF0C63-1231-442F-B800-C21760132E4D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {319FC245-1B17-4BD0-B760-FD95BA4EA2C3}
EndGlobalSection
EndGlobal

6
BokBonCheck/App.config Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
</configuration>

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{BFFF0C63-1231-442F-B800-C21760132E4D}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>BokBonCheck</RootNamespace>
<AssemblyName>BokBonCheck</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="System.IO.Compression" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Selenium.WebDriver" Version="4.16.2" />
<PackageReference Include="WebDriverManager" Version="2.17.1" />
<PackageReference Include="Selenium.Support" Version="4.16.2" />
</ItemGroup>
<ItemGroup>
<Compile Include="BookSearchService.cs" />
<Compile Include="ChromeDriverManager.cs" />
<Compile Include="DownloadProgressForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="NamguLibrarySearcher.cs" />
<Compile Include="Form1.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Form1.Designer.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<EmbeddedResource Include="Form1.resx">
<DependentUpon>Form1.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BokBonCheck", "BokBonCheck.csproj", "{BEFC3363-0F54-A638-019B-6A76908217A4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BEFC3363-0F54-A638-019B-6A76908217A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BEFC3363-0F54-A638-019B-6A76908217A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BEFC3363-0F54-A638-019B-6A76908217A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BEFC3363-0F54-A638-019B-6A76908217A4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7A76F894-89FF-4B58-8C94-36A77F1E2B44}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
namespace BokBonCheck
{
public class BookSearchResult
{
public string SiteName { get; set; }
public int BookCount { get; set; }
public string SearchTerm { get; set; }
public DateTime SearchTime { get; set; }
public string ErrorMessage { get; set; }
public bool IsSuccess { get; set; }
}
public class BookSearchService
{
private readonly List<ILibrarySearcher> _searchers;
public BookSearchService()
{
_searchers = new List<ILibrarySearcher>
{
new NamguLibrarySearcher()
// 나중에 다른 도서관 검색기를 여기에 추가할 수 있습니다
};
}
public async Task<List<BookSearchResult>> SearchBooksAsync(string searchTerm)
{
var results = new List<BookSearchResult>();
foreach (var searcher in _searchers)
{
var result = await searcher.SearchAsync(searchTerm);
results.Add(result);
}
return results;
}
public void AddSearcher(ILibrarySearcher searcher)
{
if (!_searchers.Any(s => s.SiteName == searcher.SiteName))
{
_searchers.Add(searcher);
}
}
public void RemoveSearcher(string siteName)
{
var searcher = _searchers.FirstOrDefault(s => s.SiteName == siteName);
if (searcher != null)
{
_searchers.Remove(searcher);
}
}
public List<string> GetAvailableSites()
{
return _searchers.Select(s => s.SiteName).ToList();
}
public void ClearSearchers()
{
_searchers.Clear();
}
}
}

View File

@@ -0,0 +1,378 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using WebDriverManager;
using WebDriverManager.DriverConfigs.Impl;
using System.Reflection;
using System.Net.Http;
using System.Threading;
using System.IO.Compression;
namespace BokBonCheck
{
public static class ChromeDriverManager
{
private static string _driverPath = null;
public static async Task<string> SetupChromeDriverAsync(DownloadProgressForm progressForm = null)
{
try
{
// Chrome 버전 확인
var chromeVersion = GetChromeVersion();
Console.WriteLine($"설치된 Chrome 버전: {chromeVersion}");
if (progressForm != null)
{
progressForm.UpdateProgress(10, $"Chrome 버전 확인 중... ({chromeVersion})");
}
// 기존 드라이버 경로 확인
var existingDriverPath = GetExistingDriverPath();
if (!string.IsNullOrEmpty(existingDriverPath) && File.Exists(existingDriverPath))
{
Console.WriteLine($"기존 드라이버 발견: {existingDriverPath}");
// 기존 드라이버 테스트
if (await TestExistingDriver(existingDriverPath))
{
if (progressForm != null)
{
progressForm.UpdateProgress(100, "기존 드라이버 사용");
progressForm.SetCompleted("기존 드라이버를 사용합니다.");
}
_driverPath = existingDriverPath;
Environment.SetEnvironmentVariable("webdriver.chrome.driver", existingDriverPath);
return existingDriverPath;
}
else
{
Console.WriteLine("기존 드라이버 테스트 실패 - 새로 다운로드");
// 기존 드라이버가 작동하지 않으면 삭제
try
{
File.Delete(existingDriverPath);
Console.WriteLine("기존 드라이버 삭제됨");
}
catch (Exception ex)
{
Console.WriteLine($"기존 드라이버 삭제 실패: {ex.Message}");
}
}
}
if (progressForm != null)
{
progressForm.UpdateProgress(30, "Chrome 버전에 맞는 드라이버 다운로드 중...");
}
// WebDriverManager를 사용하여 설치된 Chrome 버전에 맞는 드라이버 다운로드
var driverManager = new DriverManager();
var driverPath = driverManager.SetUpDriver(new ChromeConfig(), "MatchingBrowser");
// 다운로드된 드라이버 경로 저장
_driverPath = driverPath;
// 환경 변수 설정
Environment.SetEnvironmentVariable("webdriver.chrome.driver", driverPath);
if (progressForm != null)
{
progressForm.UpdateProgress(100, "드라이버 다운로드 완료");
progressForm.SetCompleted("드라이버 다운로드 완료!");
}
Console.WriteLine($"ChromeDriver 경로: {driverPath}");
Console.WriteLine($"환경 변수 설정: webdriver.chrome.driver = {driverPath}");
return driverPath;
}
catch (Exception ex)
{
Console.WriteLine($"ChromeDriver 설정 오류: {ex.Message}");
if (progressForm != null)
{
progressForm.SetError($"설정 오류: {ex.Message}");
}
throw;
}
}
private static string GetExistingDriverPath()
{
try
{
// 환경 변수에서 확인
var envPath = Environment.GetEnvironmentVariable("webdriver.chrome.driver");
if (!string.IsNullOrEmpty(envPath) && File.Exists(envPath))
{
return envPath;
}
// WebDriverManager 캐시 폴더에서 확인
var cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".cache", "selenium");
if (Directory.Exists(cachePath))
{
var chromeDriverFiles = Directory.GetFiles(cachePath, "chromedriver*", SearchOption.AllDirectories);
foreach (var file in chromeDriverFiles)
{
if (file.EndsWith(".exe") || !Path.HasExtension(file))
{
return file;
}
}
}
return null;
}
catch
{
return null;
}
}
public static string GetDriverPath()
{
return _driverPath;
}
private static string GetChromeVersion()
{
try
{
// Windows에서 Chrome 설치 경로 확인
var chromePaths = new[]
{
@"C:\Program Files\Google\Chrome\Application\chrome.exe",
@"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
@"Google\Chrome\Application\chrome.exe")
};
foreach (var path in chromePaths)
{
if (File.Exists(path))
{
var versionInfo = FileVersionInfo.GetVersionInfo(path);
var version = versionInfo.FileVersion;
Console.WriteLine($"Chrome 버전 감지: {version} (경로: {path})");
return version;
}
}
return "알 수 없음";
}
catch (Exception ex)
{
Console.WriteLine($"Chrome 버전 감지 실패: {ex.Message}");
return "확인 실패";
}
}
public static bool IsChromeInstalled()
{
try
{
var chromePaths = new[]
{
@"C:\Program Files\Google\Chrome\Application\chrome.exe",
@"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
@"Google\Chrome\Application\chrome.exe")
};
foreach (var path in chromePaths)
{
if (File.Exists(path))
{
return true;
}
}
return false;
}
catch
{
return false;
}
}
public static async Task<bool> TestChromeDriverAsync(DownloadProgressForm progressForm = null)
{
try
{
var driverPath = await SetupChromeDriverAsync(progressForm);
if (progressForm != null)
{
progressForm.UpdateProgress(50, "드라이버 테스트 중...");
}
// ChromeDriver 서비스 설정
var service = OpenQA.Selenium.Chrome.ChromeDriverService.CreateDefaultService(Path.GetDirectoryName(driverPath));
service.HideCommandPromptWindow = true;
// 간단한 테스트 실행
var options = new OpenQA.Selenium.Chrome.ChromeOptions();
options.AddArgument("--headless");
options.AddArgument("--no-sandbox");
options.AddArgument("--disable-dev-shm-usage");
options.AddArgument("--disable-gpu");
options.AddArgument("--remote-debugging-port=0");
using (var driver = new OpenQA.Selenium.Chrome.ChromeDriver(service, options))
{
driver.Navigate().GoToUrl("https://www.google.com");
var title = driver.Title;
Console.WriteLine($"드라이버 테스트 성공: {title}");
if (progressForm != null)
{
progressForm.UpdateProgress(100, "드라이버 테스트 성공!");
progressForm.SetCompleted("드라이버 테스트 성공!");
}
return !string.IsNullOrEmpty(title);
}
}
catch (Exception ex)
{
Console.WriteLine($"ChromeDriver 테스트 실패: {ex.Message}");
if (progressForm != null)
{
progressForm.SetError($"테스트 실패: {ex.Message}");
}
return false;
}
}
public static void ClearDriverCache()
{
try
{
Console.WriteLine("Chrome 드라이버 캐시 정리 시작...");
// WebDriverManager 캐시 폴더 정리
var cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".cache", "selenium");
if (Directory.Exists(cachePath))
{
Directory.Delete(cachePath, true);
Console.WriteLine($"WebDriverManager 캐시 정리됨: {cachePath}");
}
// 환경 변수에서 설정된 드라이버 경로도 정리
var envDriverPath = Environment.GetEnvironmentVariable("webdriver.chrome.driver");
if (!string.IsNullOrEmpty(envDriverPath) && File.Exists(envDriverPath))
{
try
{
File.Delete(envDriverPath);
Console.WriteLine($"환경 변수 드라이버 삭제됨: {envDriverPath}");
}
catch (Exception ex)
{
Console.WriteLine($"환경 변수 드라이버 삭제 실패: {ex.Message}");
}
}
// 저장된 드라이버 경로도 정리
if (!string.IsNullOrEmpty(_driverPath) && File.Exists(_driverPath))
{
try
{
File.Delete(_driverPath);
Console.WriteLine($"저장된 드라이버 삭제됨: {_driverPath}");
}
catch (Exception ex)
{
Console.WriteLine($"저장된 드라이버 삭제 실패: {ex.Message}");
}
}
_driverPath = null;
Environment.SetEnvironmentVariable("webdriver.chrome.driver", null);
Console.WriteLine("Chrome 드라이버 캐시 정리 완료");
}
catch (Exception ex)
{
Console.WriteLine($"캐시 정리 실패: {ex.Message}");
}
}
public static bool IsDriverReady()
{
try
{
var driverPath = GetExistingDriverPath();
if (string.IsNullOrEmpty(driverPath) || !File.Exists(driverPath))
{
Console.WriteLine("기존 드라이버가 없습니다.");
return false;
}
Console.WriteLine($"드라이버 준비 상태 확인: {driverPath}");
// 간단한 테스트 실행
var service = OpenQA.Selenium.Chrome.ChromeDriverService.CreateDefaultService(Path.GetDirectoryName(driverPath));
service.HideCommandPromptWindow = true;
var options = new OpenQA.Selenium.Chrome.ChromeOptions();
options.AddArgument("--headless");
options.AddArgument("--no-sandbox");
options.AddArgument("--disable-dev-shm-usage");
options.AddArgument("--disable-gpu");
options.AddArgument("--remote-debugging-port=0");
using (var driver = new OpenQA.Selenium.Chrome.ChromeDriver(service, options))
{
driver.Navigate().GoToUrl("https://www.google.com");
var result = !string.IsNullOrEmpty(driver.Title);
Console.WriteLine($"드라이버 준비 상태: {(result ? "" : "")}");
return result;
}
}
catch (Exception ex)
{
Console.WriteLine($"드라이버 준비 상태 확인 실패: {ex.Message}");
return false;
}
}
private static async Task<bool> TestExistingDriver(string driverPath)
{
try
{
Console.WriteLine($"기존 드라이버 테스트: {driverPath}");
// ChromeDriver 서비스 설정
var service = OpenQA.Selenium.Chrome.ChromeDriverService.CreateDefaultService(Path.GetDirectoryName(driverPath));
service.HideCommandPromptWindow = true;
// 간단한 테스트 실행
var options = new OpenQA.Selenium.Chrome.ChromeOptions();
options.AddArgument("--headless");
options.AddArgument("--no-sandbox");
options.AddArgument("--disable-dev-shm-usage");
options.AddArgument("--disable-gpu");
options.AddArgument("--remote-debugging-port=0");
using (var driver = new OpenQA.Selenium.Chrome.ChromeDriver(service, options))
{
driver.Navigate().GoToUrl("https://www.google.com");
var title = driver.Title;
Console.WriteLine($"드라이버 테스트 성공: {title}");
return !string.IsNullOrEmpty(title);
}
}
catch (Exception ex)
{
Console.WriteLine($"기존 드라이버 테스트 실패: {ex.Message}");
return false;
}
}
}
}

View File

@@ -0,0 +1,142 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading.Tasks;
namespace BokBonCheck
{
public partial class DownloadProgressForm : Form
{
private ProgressBar progressBar;
private Label lblStatus;
private Label lblProgress;
private Button btnCancel;
private bool isCancelled = false;
public DownloadProgressForm()
{
InitializeComponent();
this.StartPosition = FormStartPosition.CenterScreen;
}
private void InitializeComponent()
{
this.Text = "Chrome 드라이버 다운로드";
this.Size = new Size(400, 150);
this.StartPosition = FormStartPosition.CenterParent;
this.FormBorderStyle = FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.ControlBox = false;
this.TopMost = true;
// 상태 라벨
lblStatus = new Label
{
Text = "Chrome 드라이버를 확인하고 있습니다...",
Location = new Point(20, 20),
Size = new Size(360, 20),
Font = new Font("맑은 고딕", 9),
TextAlign = ContentAlignment.MiddleCenter
};
// 진행률 라벨
lblProgress = new Label
{
Text = "0%",
Location = new Point(20, 50),
Size = new Size(360, 20),
Font = new Font("맑은 고딕", 10, FontStyle.Bold),
TextAlign = ContentAlignment.MiddleCenter,
ForeColor = Color.Blue
};
// 프로그레스 바
progressBar = new ProgressBar
{
Location = new Point(20, 80),
Size = new Size(360, 25),
Style = ProgressBarStyle.Continuous,
Minimum = 0,
Maximum = 100,
Value = 0
};
// 취소 버튼
btnCancel = new Button
{
Text = "취소",
Location = new Point(150, 115),
Size = new Size(100, 30),
Font = new Font("맑은 고딕", 9),
BackColor = Color.LightCoral
};
btnCancel.Click += BtnCancel_Click;
// 컨트롤 추가
this.Controls.AddRange(new Control[]
{
lblStatus, lblProgress, progressBar, btnCancel
});
}
private void BtnCancel_Click(object sender, EventArgs e)
{
isCancelled = true;
btnCancel.Enabled = false;
lblStatus.Text = "취소 중...";
}
public bool IsCancelled => isCancelled;
public void UpdateProgress(int percentage, string status = null)
{
if (this.InvokeRequired)
{
this.Invoke(new Action<int, string>(UpdateProgress), percentage, status);
return;
}
progressBar.Value = Math.Min(percentage, 100);
lblProgress.Text = $"{percentage}%";
if (!string.IsNullOrEmpty(status))
{
lblStatus.Text = status;
}
}
public void SetCompleted(string message = "다운로드 완료!")
{
if (this.InvokeRequired)
{
this.Invoke(new Action<string>(SetCompleted), message);
return;
}
progressBar.Value = 100;
lblProgress.Text = "100%";
lblStatus.Text = message;
btnCancel.Text = "확인";
btnCancel.BackColor = Color.LightGreen;
btnCancel.Enabled = true;
}
public void SetError(string errorMessage)
{
if (this.InvokeRequired)
{
this.Invoke(new Action<string>(SetError), errorMessage);
return;
}
lblStatus.Text = "오류 발생";
lblProgress.Text = "실패";
lblProgress.ForeColor = Color.Red;
btnCancel.Text = "확인";
btnCancel.BackColor = Color.LightCoral;
btnCancel.Enabled = true;
}
}
}

47
BokBonCheck/Form1.Designer.cs generated Normal file
View File

@@ -0,0 +1,47 @@
namespace BokBonCheck
{
partial class Form1
{
/// <summary>
/// 필수 디자이너 변수입니다.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 사용 중인 모든 리소스를 정리합니다.
/// </summary>
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form
/// <summary>
/// 디자이너 지원에 필요한 메서드입니다.
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
/// </summary>
private void InitializeComponent()
{
this.SuspendLayout();
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);
}
#endregion
}
}

411
BokBonCheck/Form1.cs Normal file
View File

@@ -0,0 +1,411 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace BokBonCheck
{
public partial class Form1 : Form
{
private readonly BookSearchService _searchService;
private bool _isSearching = false;
private bool _isDriverReady = false;
public Form1()
{
InitializeComponent();
_searchService = new BookSearchService();
InitializeUI();
InitializeChromeDriver();
}
private async void InitializeChromeDriver()
{
var lblStatus = (Label)this.Controls["lblStatus"];
lblStatus.Text = "Chrome 드라이버 확인 중...";
lblStatus.ForeColor = Color.Blue;
try
{
// Chrome 설치 확인
if (!ChromeDriverManager.IsChromeInstalled())
{
MessageBox.Show("Google Chrome이 설치되어 있지 않습니다. Chrome을 설치한 후 프로그램을 다시 실행해주세요.",
"Chrome 필요", MessageBoxButtons.OK, MessageBoxIcon.Warning);
lblStatus.Text = "Chrome이 설치되지 않음";
lblStatus.ForeColor = Color.Red;
return;
}
// 기존 드라이버가 준비되어 있는지 확인
if (ChromeDriverManager.IsDriverReady())
{
lblStatus.Text = "Chrome 드라이버 준비 완료 - 검색할 준비가 되었습니다.";
lblStatus.ForeColor = Color.Green;
var btnSearch = (Button)this.Controls["btnSearch"];
btnSearch.Enabled = true;
_isDriverReady = true;
return;
}
// 드라이버가 없거나 작동하지 않으면 다운로드 진행 창 표시
using (var progressForm = new DownloadProgressForm())
{
progressForm.Show();
try
{
// ChromeDriver 설정
await ChromeDriverManager.SetupChromeDriverAsync(progressForm);
// 드라이버 테스트
var isWorking = await ChromeDriverManager.TestChromeDriverAsync(progressForm);
if (isWorking)
{
_isDriverReady = true;
lblStatus.Text = "Chrome 드라이버 준비 완료 - 검색할 준비가 되었습니다.";
lblStatus.ForeColor = Color.Green;
var btnSearch = (Button)this.Controls["btnSearch"];
btnSearch.Enabled = true;
}
else
{
lblStatus.Text = "Chrome 드라이버 테스트 실패";
lblStatus.ForeColor = Color.Red;
MessageBox.Show("Chrome 드라이버 테스트에 실패했습니다. 인터넷 연결을 확인하고 프로그램을 다시 실행해주세요.",
"드라이버 오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
catch (OperationCanceledException)
{
lblStatus.Text = "드라이버 다운로드가 취소되었습니다.";
lblStatus.ForeColor = Color.Orange;
return;
}
catch (Exception ex)
{
lblStatus.Text = "Chrome 드라이버 설정 실패";
lblStatus.ForeColor = Color.Red;
MessageBox.Show($"Chrome 드라이버 설정 중 오류가 발생했습니다: {ex.Message}",
"설정 오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
}
catch (Exception ex)
{
lblStatus.Text = "Chrome 드라이버 설정 실패";
lblStatus.ForeColor = Color.Red;
MessageBox.Show($"Chrome 드라이버 설정 중 오류가 발생했습니다: {ex.Message}",
"설정 오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void InitializeUI()
{
// 폼 설정
this.Text = "도서 검색 프로그램";
this.Size = new Size(820, 600);
this.StartPosition = FormStartPosition.CenterScreen;
// 검색 입력 영역
var lblSearch = new Label
{
Text = "도서명을 입력하세요:",
Location = new Point(20, 20),
Size = new Size(150, 25),
Font = new Font("맑은 고딕", 10)
};
var txtSearch = new TextBox
{
Name = "txtSearch",
Location = new Point(20, 50),
Size = new Size(300, 25),
Font = new Font("맑은 고딕", 10)
};
var btnSearch = new Button
{
Name = "btnSearch",
Text = "검색",
Location = new Point(340, 50),
Size = new Size(80, 25),
Font = new Font("맑은 고딕", 10),
BackColor = Color.LightBlue,
Enabled = false // 초기에는 비활성화
};
var btnClear = new Button
{
Name = "btnClear",
Text = "초기화",
Location = new Point(430, 50),
Size = new Size(80, 25),
Font = new Font("맑은 고딕", 10),
BackColor = Color.LightGray
};
var btnResetDriver = new Button
{
Name = "btnResetDriver",
Text = "드라이버 재설정",
Location = new Point(520, 50),
Size = new Size(100, 25),
Font = new Font("맑은 고딕", 9),
BackColor = Color.LightYellow
};
// 진행 상황 표시
var progressBar = new ProgressBar
{
Name = "progressBar",
Location = new Point(20, 90),
Size = new Size(600, 20),
Visible = false
};
var lblStatus = new Label
{
Name = "lblStatus",
Text = "Chrome 드라이버 설정 중...",
Location = new Point(20, 120),
Size = new Size(490, 25),
Font = new Font("맑은 고딕", 9),
ForeColor = Color.Blue
};
// 결과 표시 영역
var lblResults = new Label
{
Text = "검색 결과:",
Location = new Point(20, 160),
Size = new Size(150, 25),
Font = new Font("맑은 고딕", 10, FontStyle.Bold)
};
var dataGridView = new DataGridView
{
Name = "dataGridView",
Location = new Point(20, 190),
Size = new Size(760, 350),
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
ReadOnly = true,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
BackgroundColor = Color.White
};
// 컨트롤 추가
this.Controls.AddRange(new Control[]
{
lblSearch, txtSearch, btnSearch, btnClear, btnResetDriver,
progressBar, lblStatus, lblResults, dataGridView
});
// 이벤트 핸들러 연결
btnSearch.Click += BtnSearch_Click;
btnClear.Click += BtnClear_Click;
btnResetDriver.Click += BtnResetDriver_Click;
txtSearch.KeyPress += TxtSearch_KeyPress;
// DataGridView 설정
SetupDataGridView();
}
private void SetupDataGridView()
{
var dataGridView = (DataGridView)this.Controls["dataGridView"];
dataGridView.Columns.Clear();
dataGridView.Columns.Add("SiteName", "도서관");
dataGridView.Columns.Add("BookCount", "도서 수");
dataGridView.Columns.Add("SearchTime", "검색 시간");
dataGridView.Columns.Add("Status", "상태");
// 컬럼 너비 설정
dataGridView.Columns["SiteName"].Width = 150;
dataGridView.Columns["BookCount"].Width = 100;
dataGridView.Columns["SearchTime"].Width = 150;
dataGridView.Columns["Status"].Width = 100;
}
private async void BtnSearch_Click(object sender, EventArgs e)
{
if (_isSearching || !_isDriverReady) return;
var txtSearch = (TextBox)this.Controls["txtSearch"];
var btnSearch = (Button)this.Controls["btnSearch"];
var progressBar = (ProgressBar)this.Controls["progressBar"];
var lblStatus = (Label)this.Controls["lblStatus"];
var dataGridView = (DataGridView)this.Controls["dataGridView"];
if (string.IsNullOrWhiteSpace(txtSearch.Text))
{
MessageBox.Show("도서명을 입력해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
_isSearching = true;
btnSearch.Enabled = false;
progressBar.Visible = true;
progressBar.Style = ProgressBarStyle.Marquee;
lblStatus.Text = "검색 중...";
lblStatus.ForeColor = Color.Blue;
try
{
// 기존 결과 초기화
dataGridView.Rows.Clear();
// 검색 실행
var results = await _searchService.SearchBooksAsync(txtSearch.Text.Trim());
// 결과 표시
foreach (var result in results)
{
var row = new object[]
{
result.SiteName,
result.IsSuccess ? result.BookCount.ToString() : "오류",
result.SearchTime.ToString("yyyy-MM-dd HH:mm:ss"),
result.IsSuccess ? "성공" : "실패"
};
dataGridView.Rows.Add(row);
// 실패한 경우 오류 메시지 표시
if (!result.IsSuccess)
{
MessageBox.Show($"{result.SiteName}: {result.ErrorMessage}", "검색 오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
lblStatus.Text = $"검색 완료 - {results.Count}개 사이트 검색됨";
lblStatus.ForeColor = Color.Green;
}
catch (Exception ex)
{
lblStatus.Text = "검색 중 오류가 발생했습니다.";
lblStatus.ForeColor = Color.Red;
MessageBox.Show($"검색 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
_isSearching = false;
btnSearch.Enabled = _isDriverReady;
progressBar.Visible = false;
}
}
private void BtnClear_Click(object sender, EventArgs e)
{
var txtSearch = (TextBox)this.Controls["txtSearch"];
var dataGridView = (DataGridView)this.Controls["dataGridView"];
var lblStatus = (Label)this.Controls["lblStatus"];
txtSearch.Clear();
dataGridView.Rows.Clear();
if (_isDriverReady)
{
lblStatus.Text = "검색할 준비가 되었습니다.";
lblStatus.ForeColor = Color.Green;
}
else
{
lblStatus.Text = "Chrome 드라이버 설정 실패";
lblStatus.ForeColor = Color.Red;
}
}
private async void BtnResetDriver_Click(object sender, EventArgs e)
{
var btnResetDriver = (Button)sender;
var lblStatus = (Label)this.Controls["lblStatus"];
btnResetDriver.Enabled = false;
lblStatus.Text = "드라이버 재설정 중...";
lblStatus.ForeColor = Color.Blue;
try
{
// 강제로 캐시 정리 (재설정이므로)
ChromeDriverManager.ClearDriverCache();
// 다운로드 진행 창 표시
using (var progressForm = new DownloadProgressForm())
{
progressForm.Text = "Chrome 드라이버 재설정";
progressForm.Show();
try
{
// 드라이버 재설정
await ChromeDriverManager.SetupChromeDriverAsync(progressForm);
// 테스트
var isWorking = await ChromeDriverManager.TestChromeDriverAsync(progressForm);
if (isWorking)
{
_isDriverReady = true;
lblStatus.Text = "드라이버 재설정 완료 - 검색할 준비가 되었습니다.";
lblStatus.ForeColor = Color.Green;
var btnSearch = (Button)this.Controls["btnSearch"];
btnSearch.Enabled = true;
}
else
{
lblStatus.Text = "드라이버 재설정 실패";
lblStatus.ForeColor = Color.Red;
MessageBox.Show("드라이버 재설정에 실패했습니다.", "재설정 실패", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
catch (OperationCanceledException)
{
lblStatus.Text = "드라이버 재설정이 취소되었습니다.";
lblStatus.ForeColor = Color.Orange;
return;
}
catch (Exception ex)
{
lblStatus.Text = "드라이버 재설정 중 오류 발생";
lblStatus.ForeColor = Color.Red;
MessageBox.Show($"드라이버 재설정 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
}
catch (Exception ex)
{
lblStatus.Text = "드라이버 재설정 중 오류 발생";
lblStatus.ForeColor = Color.Red;
MessageBox.Show($"드라이버 재설정 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
btnResetDriver.Enabled = true;
}
}
private void TxtSearch_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)Keys.Enter)
{
e.Handled = true;
BtnSearch_Click(sender, e);
}
}
}
}

120
BokBonCheck/Form1.resx Normal file
View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,203 @@
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 NamguLibrarySearcher : ILibrarySearcher
{
public string SiteName => "남구통합도서관";
public string SiteUrl => "https://lib.namgu.gwangju.kr/main/bookSearch";
public async Task<BookSearchResult> 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));
// 검색창 찾기 (남구통합도서관 사이트의 특정 선택자 사용)
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 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.search-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;
}
}
}
public interface ILibrarySearcher
{
string SiteName { get; }
string SiteUrl { get; }
Task<BookSearchResult> SearchAsync(string searchTerm);
}
}

22
BokBonCheck/Program.cs Normal file
View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace BokBonCheck
{
internal static class Program
{
/// <summary>
/// 해당 애플리케이션의 주 진입점입니다.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}

View File

@@ -0,0 +1,33 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 어셈블리에 대한 일반 정보는 다음 특성 집합을 통해
// 제어됩니다. 어셈블리와 관련된 정보를 수정하려면
// 이러한 특성 값을 변경하세요.
[assembly: AssemblyTitle("BokBonCheck")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("BokBonCheck")]
[assembly: AssemblyCopyright("Copyright © 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// ComVisible을 false로 설정하면 이 어셈블리의 형식이 COM 구성 요소에
// 표시되지 않습니다. COM에서 이 어셈블리의 형식에 액세스하려면
// 해당 형식에 대해 ComVisible 특성을 true로 설정하세요.
[assembly: ComVisible(false)]
// 이 프로젝트가 COM에 노출되는 경우 다음 GUID는 typelib의 ID를 나타냅니다.
[assembly: Guid("bfff0c63-1231-442f-b800-c21760132e4d")]
// 어셈블리의 버전 정보는 다음 네 가지 값으로 구성됩니다.
//
// 주 버전
// 부 버전
// 빌드 번호
// 수정 버전
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 이 코드는 도구를 사용하여 생성되었습니다.
// 런타임 버전:4.0.30319.42000
//
// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
// 이러한 변경 내용이 손실됩니다.
// </auto-generated>
//------------------------------------------------------------------------------
namespace BokBonCheck.Properties
{
/// <summary>
/// 지역화된 문자열 등을 찾기 위한 강력한 형식의 리소스 클래스입니다.
/// </summary>
// 이 클래스는 ResGen 또는 Visual Studio와 같은 도구를 통해 StronglyTypedResourceBuilder
// 클래스에서 자동으로 생성되었습니다.
// 멤버를 추가하거나 제거하려면 .ResX 파일을 편집한 다음 /str 옵션을 사용하여
// ResGen을 다시 실행하거나 VS 프로젝트를 다시 빌드하십시오.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
}
/// <summary>
/// 이 클래스에서 사용하는 캐시된 ResourceManager 인스턴스를 반환합니다.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BokBonCheck.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// 이 강력한 형식의 리소스 클래스를 사용하여 모든 리소스 조회에 대해 현재 스레드의 CurrentUICulture 속성을
/// 재정의합니다.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace BokBonCheck.Properties
{
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default
{
get
{
return defaultInstance;
}
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

134
BokBonCheck/README.md Normal file
View File

@@ -0,0 +1,134 @@
# 도서 검색 프로그램 (BokBonCheck)
## 개요
이 프로그램은 여러 도서관 웹사이트에서 도서명을 검색하여 각 사이트별로 검색된 도서 수를 확인할 수 있는 C# WinForms 애플리케이션입니다.
## 주요 기능
- **다중 사이트 검색**: 여러 도서관 사이트에서 동시에 도서 검색
- **실시간 결과 표시**: 검색 결과를 테이블 형태로 실시간 표시
- **확장 가능한 구조**: 새로운 도서관 사이트를 쉽게 추가 가능
- **비동기 처리**: 검색 중에도 UI가 응답 가능
- **자동 Chrome 드라이버 관리**: Chrome 버전에 맞는 드라이버 자동 다운로드 및 관리
## 현재 지원하는 도서관
- 남구통합도서관 (https://lib.namgu.gwangju.kr/main/bookSearch)
## 기술 스택
- **.NET Framework 4.8**
- **C# WinForms**
- **Selenium WebDriver**
- **WebDriverManager** (Chrome 드라이버 자동 관리)
## 설치 및 실행
### 필수 요구사항
1. .NET Framework 4.8 이상
2. Google Chrome 브라우저 (최신 버전 권장)
3. 인터넷 연결 (Chrome 드라이버 다운로드용)
4. Visual Studio 2019 이상 (개발용)
### 빌드 및 실행
1. 프로젝트를 Visual Studio에서 열기
2. NuGet 패키지 복원 (자동으로 실행됨)
3. 프로젝트 빌드 (Ctrl+Shift+B)
4. 실행 (F5)
### 배포
1. Release 모드로 빌드
2. `bin/Release` 폴더의 모든 파일을 배포 대상 폴더에 복사
3. BokBonCheck.exe 실행
## 사용법
### 프로그램 시작
1. 프로그램 실행 시 Chrome 드라이버 자동 설정
2. Chrome 설치 확인 및 적절한 드라이버 다운로드
3. 드라이버 테스트 완료 후 검색 가능
### 기본 검색
1. 검색창에 도서명 입력
2. "검색" 버튼 클릭 또는 Enter 키 입력
3. 검색 결과 확인
### 결과 해석
- **도서관**: 검색한 도서관 이름
- **도서 수**: 해당 도서관에서 검색된 도서의 총 개수
- **검색 시간**: 검색이 완료된 시간
- **상태**: 검색 성공/실패 여부
## 프로젝트 구조
```
BokBonCheck/
├── Form1.cs # 메인 UI 폼
├── BookSearchService.cs # 검색 서비스 관리
├── NamguLibrarySearcher.cs # 남구통합도서관 전용 검색기
├── ChromeDriverManager.cs # Chrome 드라이버 자동 관리
├── ILibrarySearcher.cs # 검색기 인터페이스
└── Program.cs # 프로그램 진입점
```
## Chrome 드라이버 자동 관리
### WebDriverManager 기능
- **자동 버전 감지**: 설치된 Chrome 버전 자동 감지
- **드라이버 자동 다운로드**: Chrome 버전에 맞는 드라이버 자동 다운로드
- **캐시 관리**: 다운로드된 드라이버 캐시 관리
- **업데이트 지원**: Chrome 업데이트 시 드라이버 자동 업데이트
### 동작 과정
1. 프로그램 시작 시 Chrome 설치 확인
2. Chrome 버전 정보 수집
3. WebDriverManager를 통한 적절한 드라이버 다운로드
4. 드라이버 테스트 실행
5. 검색 기능 활성화
## 새로운 도서관 추가하기
새로운 도서관을 추가하려면 `ILibrarySearcher` 인터페이스를 구현하는 클래스를 생성하고 `BookSearchService`에 등록하면 됩니다.
### 예시:
```csharp
public class NewLibrarySearcher : ILibrarySearcher
{
public string SiteName => "새로운도서관";
public string SiteUrl => "https://newlibrary.com/search";
public async Task<BookSearchResult> SearchAsync(string searchTerm)
{
// 검색 로직 구현
}
}
```
그리고 `BookSearchService` 생성자에서:
```csharp
_searchers.Add(new NewLibrarySearcher());
```
## 문제 해결
### Chrome 관련 오류
- **Chrome 미설치**: Google Chrome을 설치하고 프로그램 재실행
- **드라이버 다운로드 실패**: 인터넷 연결 확인 후 프로그램 재실행
- **드라이버 테스트 실패**: 방화벽이나 보안 프로그램 설정 확인
### 검색 실패
- 인터넷 연결 상태 확인
- 해당 도서관 사이트가 정상 작동하는지 확인
- 사이트 구조가 변경되었을 수 있으므로 검색 로직 업데이트 필요
### 성능 문제
- 검색 중에는 다른 검색을 시작하지 마세요
- 네트워크 상태에 따라 검색 시간이 달라질 수 있습니다
- 첫 실행 시 드라이버 다운로드로 인한 지연이 있을 수 있습니다
## 업데이트 내역
- v1.1.0: Chrome 드라이버 자동 관리 기능 추가 (WebDriverManager 사용)
- v1.0.0: 초기 버전 - 남구통합도서관 검색 기능 구현
## 라이선스
이 프로젝트는 개인 및 교육 목적으로 사용할 수 있습니다.
## 기여
버그 리포트나 기능 제안은 이슈로 등록해 주세요.