351 lines
12 KiB
Markdown
351 lines
12 KiB
Markdown
# ⚠️ 중요: 대화 시작시 이 파일을 반드시 읽으세요!
|
|
# 답변은 가급적이면 한글로!
|
|
# UniMarc 프로젝트 - Claude 작업 가이드
|
|
|
|
> **Claude에게**: 대화를 시작할 때마다 이 파일을 먼저 읽어서 프로젝트 컨텍스트를 파악하세요.
|
|
|
|
## 프로젝트 개요
|
|
- **프로젝트명**: UniMarc (도서관 자료 관리 시스템)
|
|
- **기술스택**: C# WinForms, .NET Framework 4.7.2
|
|
- **데이터베이스**: MySQL
|
|
- **주요기능**: 마크 작성, 복본조사, DLS 연동, 도서 정보 관리
|
|
|
|
## 코딩 컨벤션
|
|
- 파일명: PascalCase (예: DLS_Copy.cs)
|
|
- 클래스명: PascalCase
|
|
- 메서드명: PascalCase
|
|
- 변수명: camelCase
|
|
- 상수명: UPPER_CASE
|
|
|
|
## 주요 디렉토리 구조
|
|
- `/마크/`: 마크 관련 폼들
|
|
- `/납품관리/`: 납품 관리 관련 폼들
|
|
- `/마스터/`: 마스터 데이터 관리 폼들
|
|
- `/홈/`: 메인 화면 관련 폼들
|
|
- `/회계/`: 회계 관련 폼들
|
|
|
|
## 개발 시 주의사항
|
|
1. WebView2 사용 시 async/await 패턴 적용
|
|
2. 데이터베이스 연결은 Helper_DB 클래스 사용
|
|
3. 에러 처리는 try-catch 블록으로 처리
|
|
4. 한글 주석 사용
|
|
|
|
## 빌드 및 배포
|
|
- 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 다운로드 주소
|
|
https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/759b508a-00bb-4724-9b87-2703c8417737/Microsoft.WebView2.FixedVersionRuntime.139.0.3405.86.x86.cab
|
|
|
|
## WebView2 Selenium 스타일 DOM 조작 가이드
|
|
|
|
### 🔧 기본 DOM 조작 메서드들 (NamguLibrarySearcher 기본 클래스에 구현됨)
|
|
|
|
#### 1. 요소 대기 및 존재 확인
|
|
```csharp
|
|
// DOM 요소가 준비될 때까지 대기 (Selenium의 WebDriverWait와 유사)
|
|
await WaitForElementReady("#clickAll", 10000); // 10초 타임아웃
|
|
await WaitForElementReady("input[name='query']", 5000); // 5초 타임아웃
|
|
```
|
|
|
|
#### 2. 요소 상태 확인
|
|
```csharp
|
|
// 체크박스가 체크되어 있는지 확인 (Selenium의 element.IsSelected와 유사)
|
|
bool isChecked = await IsElementChecked("#clickAll");
|
|
bool isLibMAChecked = await IsElementChecked("#libMA");
|
|
```
|
|
|
|
#### 3. 요소 클릭
|
|
```csharp
|
|
// 요소 클릭 (Selenium의 element.Click()와 유사)
|
|
await ClickElement("#clickAll"); // 전체 선택 클릭
|
|
await ClickElement("#libMA"); // 문화정보도서관 클릭
|
|
await ClickElement("button[type='submit']"); // 검색 버튼 클릭
|
|
```
|
|
|
|
#### 4. 값 입력 및 설정
|
|
```csharp
|
|
// 입력창에 값 설정 (Selenium의 element.SendKeys()와 유사)
|
|
await SetElementValue("input[name='query']", "검색어");
|
|
await SetElementValue("#search_txt", "도서명");
|
|
// 이벤트도 자동으로 발생시킴 (input, change 이벤트)
|
|
```
|
|
|
|
#### 5. 텍스트 가져오기
|
|
```csharp
|
|
// 요소의 텍스트 내용 가져오기 (Selenium의 element.Text와 유사)
|
|
string resultText = await GetElementText(".search-result span");
|
|
string bookCount = await GetElementText("span:contains('전체')");
|
|
```
|
|
|
|
### 🚀 실제 사용 예제
|
|
|
|
#### 도서관 선택 (남구통합도서관 예제)
|
|
```csharp
|
|
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;
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 검색 실행 (단계별)
|
|
```csharp
|
|
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 선택자 참고
|
|
|
|
#### 남구통합도서관 주요 선택자들
|
|
```css
|
|
/* 도서관 선택 체크박스 */
|
|
#clickAll /* 전체 선택 */
|
|
#libMA /* 문화정보도서관 */
|
|
#libMB /* 푸른길도서관 */
|
|
#libMC /* 청소년도서관 */
|
|
#libSW /* 효천어울림도서관 */
|
|
#libSQ /* 스마트도서관 */
|
|
|
|
/* 검색 관련 */
|
|
input[name='query'] /* 검색창 */
|
|
button[type='submit'] /* 검색 버튼 */
|
|
|
|
/* 결과 관련 */
|
|
.search-result /* 검색 결과 영역 */
|
|
span:contains('전체') /* 검색 결과 수량 */
|
|
```
|
|
|
|
### ⚠️ 주의사항
|
|
|
|
1. **페이지 로딩 대기**: 항상 `WaitForElementReady()`로 요소가 준비될 때까지 대기
|
|
2. **에러 처리**: try-catch로 각 단계별 예외 처리
|
|
3. **로깅**: `Console.WriteLine()`으로 상세한 실행 로그 남기기
|
|
4. **타임아웃**: 적절한 타임아웃 설정 (기본 10초)
|
|
5. **다중 선택자**: 여러 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 반환**
|
|
|
|
**❌ 피해야 할 패턴:**
|
|
```csharp
|
|
// 복잡한 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 참조 오류 발생 가능
|
|
```
|
|
|
|
**✅ 권장하는 안전한 패턴:**
|
|
```csharp
|
|
// 단순한 문자열 반환 - 안정적임
|
|
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;
|
|
}
|
|
```
|
|
|
|
### **핵심 교훈:**
|
|
|
|
1. **단순함이 최고**: WebView2에서는 복잡한 JSON.stringify()보다 단순한 문자열 반환이 훨씬 안정적
|
|
2. **Dynamic 타입 위험**: `dynamic` 객체는 null 체크 없이 사용하면 RuntimeBinderException 발생
|
|
3. **직접적인 JavaScript**: 중간 JSON 변환 없이 직접적인 DOM 조작이 더 확실함
|
|
4. **단계별 진단**: 복잡한 로직은 여러 개의 간단한 스크립트로 분할하여 실행
|
|
|
|
### **권장 디버깅 패턴:**
|
|
```csharp
|
|
// 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는 메모리 누수로 인해 프로그램이 갑자기 종료될 수 있습니다.
|
|
|
|
**해결책**: 검색 전후로 메모리 정리를 수행하세요.
|
|
|
|
```csharp
|
|
// 검색 시작 전
|
|
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보다 메모리 관리가 까다로움
|
|
|