12 KiB
12 KiB
⚠️ 중요: 대화 시작시 이 파일을 반드시 읽으세요!
답변은 가급적이면 한글로!
UniMarc 프로젝트 - Claude 작업 가이드
Claude에게: 대화를 시작할 때마다 이 파일을 먼저 읽어서 프로젝트 컨텍스트를 파악하세요.
프로젝트 개요
- 프로젝트명: UniMarc (도서관 자료 관리 시스템)
- 기술스택: C# WinForms, .NET Framework 4.7.2
- 데이터베이스: MySQL
- 주요기능: 마크 작성, 복본조사, DLS 연동, 도서 정보 관리
코딩 컨벤션
- 파일명: PascalCase (예: DLS_Copy.cs)
- 클래스명: PascalCase
- 메서드명: PascalCase
- 변수명: camelCase
- 상수명: UPPER_CASE
주요 디렉토리 구조
/마크/: 마크 관련 폼들/납품관리/: 납품 관리 관련 폼들/마스터/: 마스터 데이터 관리 폼들/홈/: 메인 화면 관련 폼들/회계/: 회계 관련 폼들
개발 시 주의사항
- WebView2 사용 시 async/await 패턴 적용
- 데이터베이스 연결은 Helper_DB 클래스 사용
- 에러 처리는 try-catch 블록으로 처리
- 한글 주석 사용
빌드 및 배포
- Visual Studio 2019 이상 필요
- NuGet 패키지 복원 후 빌드
- WebView2 런타임 필요
- NetFX 프로젝트이므로 dotnet 명령은 사용 불가
MsBuild 실행파일 위치 (경로에 공백이 있으니 쌍따옴표로 감싸야 함)
매개변수 입력할때 platform 은 제거하고 그냥 프로젝트명만 입력
F:(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\msbuild.exe
프로젝트 파일명
UniMarc.csproj
Webview2 Fixed Version 다운로드 주소
WebView2 Selenium 스타일 DOM 조작 가이드
🔧 기본 DOM 조작 메서드들 (NamguLibrarySearcher 기본 클래스에 구현됨)
1. 요소 대기 및 존재 확인
// DOM 요소가 준비될 때까지 대기 (Selenium의 WebDriverWait와 유사)
await WaitForElementReady("#clickAll", 10000); // 10초 타임아웃
await WaitForElementReady("input[name='query']", 5000); // 5초 타임아웃
2. 요소 상태 확인
// 체크박스가 체크되어 있는지 확인 (Selenium의 element.IsSelected와 유사)
bool isChecked = await IsElementChecked("#clickAll");
bool isLibMAChecked = await IsElementChecked("#libMA");
3. 요소 클릭
// 요소 클릭 (Selenium의 element.Click()와 유사)
await ClickElement("#clickAll"); // 전체 선택 클릭
await ClickElement("#libMA"); // 문화정보도서관 클릭
await ClickElement("button[type='submit']"); // 검색 버튼 클릭
4. 값 입력 및 설정
// 입력창에 값 설정 (Selenium의 element.SendKeys()와 유사)
await SetElementValue("input[name='query']", "검색어");
await SetElementValue("#search_txt", "도서명");
// 이벤트도 자동으로 발생시킴 (input, change 이벤트)
5. 텍스트 가져오기
// 요소의 텍스트 내용 가져오기 (Selenium의 element.Text와 유사)
string resultText = await GetElementText(".search-result span");
string bookCount = await GetElementText("span:contains('전체')");
🚀 실제 사용 예제
도서관 선택 (남구통합도서관 예제)
protected override async Task<bool> SelectLibraryWebView2()
{
try
{
// 1. DOM 요소 존재 확인 및 대기
await WaitForElementReady("#clickAll");
await WaitForElementReady("#libMA");
// 2. 전체 선택 해제 (단계별 실행)
bool isClickAllChecked = await IsElementChecked("#clickAll");
if (isClickAllChecked)
{
await ClickElement("#clickAll");
Console.WriteLine("전체 선택 해제됨");
}
// 3. 특정 도서관 선택
bool libMAChecked = await ClickElement("#libMA");
Console.WriteLine($"문화정보도서관 선택: {libMAChecked}");
return libMAChecked;
}
catch (Exception ex)
{
Console.WriteLine($"도서관 선택 오류: {ex.Message}");
return false;
}
}
검색 실행 (단계별)
private async Task PerformSearchWebView2(string searchTerm)
{
try
{
// 1. 검색창 찾기 및 대기 (여러 선택자 시도)
string[] searchSelectors = {
"input[name='query']",
"input[id='query']",
"input[type='text']"
};
string searchInputSelector = null;
foreach (var selector in searchSelectors)
{
try
{
await WaitForElementReady(selector, 3000);
searchInputSelector = selector;
break;
}
catch { continue; }
}
// 2. 검색어 입력
await SetElementValue(searchInputSelector, searchTerm);
// 3. 검색 버튼 클릭 (여러 선택자 시도)
string[] buttonSelectors = {
"button[type='submit']",
"input[type='submit']",
".search-btn",
".btn-search"
};
bool searchExecuted = false;
foreach (var selector in buttonSelectors)
{
if (await ClickElement(selector))
{
searchExecuted = true;
break;
}
}
// 4. 검색 버튼이 없으면 Enter 키로 검색
if (!searchExecuted)
{
string enterScript = $@"
const input = document.querySelector('{searchInputSelector}');
if (input) {{
input.dispatchEvent(new KeyboardEvent('keydown', {{
key: 'Enter', keyCode: 13, bubbles: true
}}));
return true;
}}
return false;
";
await _webView2.CoreWebView2.ExecuteScriptAsync(enterScript);
}
Console.WriteLine("✅ 검색 실행 완료");
}
catch (Exception ex)
{
Console.WriteLine($"❌ 검색 실행 오류: {ex.Message}");
throw;
}
}
🎯 CSS 선택자 참고
남구통합도서관 주요 선택자들
/* 도서관 선택 체크박스 */
#clickAll /* 전체 선택 */
#libMA /* 문화정보도서관 */
#libMB /* 푸른길도서관 */
#libMC /* 청소년도서관 */
#libSW /* 효천어울림도서관 */
#libSQ /* 스마트도서관 */
/* 검색 관련 */
input[name='query'] /* 검색창 */
button[type='submit'] /* 검색 버튼 */
/* 결과 관련 */
.search-result /* 검색 결과 영역 */
span:contains('전체') /* 검색 결과 수량 */
⚠️ 주의사항
- 페이지 로딩 대기: 항상
WaitForElementReady()로 요소가 준비될 때까지 대기 - 에러 처리: try-catch로 각 단계별 예외 처리
- 로깅:
Console.WriteLine()으로 상세한 실행 로그 남기기 - 타임아웃: 적절한 타임아웃 설정 (기본 10초)
- 다중 선택자: 여러 CSS 선택자를 배열로 준비하여 순차적으로 시도
🔄 Selenium에서 WebView2로 이주 가이드
| Selenium 코드 | WebView2 대응 코드 |
|---|---|
WebDriverWait.Until() |
await WaitForElementReady() |
element.Click() |
await ClickElement() |
element.SendKeys() |
await SetElementValue() |
element.Text |
await GetElementText() |
element.IsSelected |
await IsElementChecked() |
element.Clear() |
SetElementValue로 빈 문자열 설정 |
💡 성능 및 안정성 팁
- WebView2가 Selenium보다 빠름: 네이티브 성능
- 고정 버전 사용: Chrome 버전 호환성 문제 없음
- 단계별 실행: 각 작업을 개별 메서드로 분리하여 디버깅 용이
- 상세 로깅: 각 단계마다 성공/실패 상태 출력
- 단순한 JavaScript 사용: 복잡한 JSON보다는 단순한 문자열 반환 권장
🚨 WebView2 JavaScript 실행 시 주의사항 (중요 교훈)
문제: RuntimeBinderException과 null 반환
❌ 피해야 할 패턴:
// 복잡한 JSON 반환 스크립트 - 불안정함
string script = $@"
try {{
const element = document.querySelector('{selector}');
return JSON.stringify({{ success: true, isChecked: element.checked }});
}} catch (e) {{
return JSON.stringify({{ success: false, error: e.message }});
}}
";
string result = await webView2.CoreWebView2.ExecuteScriptAsync(script);
dynamic resultObj = JsonConvert.DeserializeObject(result); // RuntimeBinderException 위험!
if (resultObj.success == true) // null 참조 오류 발생 가능
✅ 권장하는 안전한 패턴:
// 단순한 문자열 반환 - 안정적임
string result = await webView2.CoreWebView2.ExecuteScriptAsync($@"
try {{
var element = document.querySelector('{selector}');
if (element && element.checked) {{
element.click();
return element.checked ? 'failed' : 'success';
}}
return element ? 'already_unchecked' : 'not_found';
}} catch (e) {{
return 'error: ' + e.message;
}}
");
// 안전한 문자열 비교
if (result != null && result.Contains("success"))
{
return true;
}
핵심 교훈:
- 단순함이 최고: WebView2에서는 복잡한 JSON.stringify()보다 단순한 문자열 반환이 훨씬 안정적
- Dynamic 타입 위험:
dynamic객체는 null 체크 없이 사용하면 RuntimeBinderException 발생 - 직접적인 JavaScript: 중간 JSON 변환 없이 직접적인 DOM 조작이 더 확실함
- 단계별 진단: 복잡한 로직은 여러 개의 간단한 스크립트로 분할하여 실행
권장 디버깅 패턴:
// 1단계: 기본 JavaScript 실행 확인
string test1 = await webView2.ExecuteScriptAsync("1+1");
Console.WriteLine($"기본 연산: {test1}");
// 2단계: DOM 접근 확인
string test2 = await webView2.ExecuteScriptAsync("document.title");
Console.WriteLine($"DOM 접근: {test2}");
// 3단계: 요소 존재 확인
string test3 = await webView2.ExecuteScriptAsync($"document.querySelector('{selector}') !== null");
Console.WriteLine($"요소 존재: {test3}");
// 4단계: 실제 작업 수행
string result = await webView2.ExecuteScriptAsync($"document.querySelector('{selector}').click(); 'clicked'");
Console.WriteLine($"작업 결과: {result}");
이 패턴을 사용하면 WebView2 JavaScript 실행에서 99%의 문제를 예방할 수 있습니다!
🧹 WebView2 메모리 누수 방지 (중요!)
문제: WebView2는 메모리 누수로 인해 프로그램이 갑자기 종료될 수 있습니다.
해결책: 검색 전후로 메모리 정리를 수행하세요.
// 검색 시작 전
await CleanupWebView2Memory();
// 검색 작업 수행...
// 검색 완료 후 (finally 블록에서)
finally {
await CleanupWebView2Memory();
}
private async Task CleanupWebView2Memory()
{
try {
await _webView2.CoreWebView2.ExecuteScriptAsync(@"
if (window.gc) window.gc(); null;
");
GC.Collect();
GC.WaitForPendingFinalizers();
} catch { /* 무시 */ }
}
주의사항:
- WebView2는 장시간 사용 시 메모리 누수 발생 가능
- 각 검색 작업 후 반드시 정리 수행
- Chrome WebDriver보다 메모리 관리가 까다로움