From 400a66daabb989b292862c5717457855e20346bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Thu, 5 Feb 2026 08:58:53 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EA=B1=B0=EB=9E=98=EC=B2=98=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=8B=9C=EB=82=98=EB=A6=AC=EC=98=A4=20=EC=8B=A4?= =?UTF-8?q?=EC=A0=9C=20UI=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 필터: select_filter → Radix combobox evaluate 스크립트로 변경 - 저장: 확인 다이얼로그 제거 (직접 저장 후 목록 리다이렉트) - capture 액션 → evaluate로 변경 (native_required 방지) - 행 클릭: click_row → evaluate 기반 직접 클릭으로 변경 - URL 패턴: /ko/ 접두사 제거 - 컬럼 수: 11 → 10개로 수정 - 원복 스텝: 다이얼로그 없는 직접 저장 패턴 적용 --- vendor-management.json | 258 +++++++++++++++++++++++++++-------------- 1 file changed, 168 insertions(+), 90 deletions(-) diff --git a/vendor-management.json b/vendor-management.json index 0a4849f..c18a04d 100644 --- a/vendor-management.json +++ b/vendor-management.json @@ -9,13 +9,13 @@ "baseUrl": "https://dev.codebridge-x.com", "navigation": { "targetUrl": "/accounting/vendors", - "urlPattern": "/accounting/vendors|/ko/accounting/vendors", + "urlPattern": "/accounting/vendors", "menuHints": ["거래처관리", "거래처 관리", "회계관리"] }, "menuNavigation": { "level1": "회계관리", "level2": "거래처관리", - "expectedUrl": "/ko/accounting/vendors", + "expectedUrl": "/accounting/vendors", "searchWithinParent": true, "closeOtherMenus": true }, @@ -40,7 +40,12 @@ }, "notes": { "skip": ["등록 버튼 (추후 구현 예정)", "삭제 기능 (보류)"], - "focus": ["테이블 행 클릭 → 상세 페이지", "수정 모드 진입", "수정 후 저장"] + "focus": ["테이블 행 클릭 → 상세 페이지", "수정 모드 진입", "수정 후 저장"], + "uiNotes": [ + "필터 드롭다운: Radix UI Select (button[role='combobox'])", + "체크박스: Radix UI Checkbox (button[role='checkbox'])", + "저장: 확인 다이얼로그 없이 직접 저장 후 목록으로 리다이렉트" + ] }, "steps": [ { @@ -87,7 +92,7 @@ { "type": "wait", "target": "페이지 로드 완료", "timeout": 10000 } ], "expect": { - "url": "/ko/accounting/vendors", + "url": "/accounting/vendors", "pageTitle": "거래처관리", "elements": ["통계 카드", "테이블", "검색창"] }, @@ -102,8 +107,8 @@ "name": "필수 검증 #5: 목업 페이지 감지", "action": "verify_not_mockup", "checks": [ - "검색 입력 필드 존재", - "필터 드롭다운 존재 (구분, 신용등급, 거래등급, 악성채권, 정렬)", + "검색 입력 필드 존재 (placeholder: 거래처명, 거래처코드, 사업자번호 검색...)", + "필터 드롭다운 존재 (구분, 신용등급, 거래등급, 악성채권 - Radix combobox)", "테이블 데이터 표시", "API 호출 확인" ], @@ -125,7 +130,7 @@ "name": "테이블 구조 확인", "action": "verify_table", "checks": [ - "체크박스 컬럼", + "체크박스 컬럼 (Radix button[role='checkbox'])", "번호 컬럼", "구분 컬럼 (매출/매입/매입매출)", "거래처명 컬럼", @@ -134,10 +139,9 @@ "신용등급 컬럼", "거래등급 컬럼", "미수금 컬럼", - "악성채권 컬럼", - "작업 컬럼" + "악성채권 컬럼" ], - "expected": "11개 컬럼 존재" + "expected": "10개 컬럼 존재" }, { "id": 6, @@ -145,73 +149,113 @@ "critical": true, "description": "검색어 입력 후 테이블 데이터가 필터링되는지 확인", "actions": [ - { "type": "capture", "variable": "beforeSearchCount", "selector": "table tbody tr", "extract": "count", "description": "검색 전 행 수 저장" }, - { "type": "fill", "target": "검색", "value": "가우스", "description": "검색어 '가우스' 입력" }, - { "type": "wait", "duration": 500, "description": "검색 결과 대기" }, - { "type": "capture", "variable": "afterSearchCount", "selector": "table tbody tr", "extract": "count", "description": "검색 후 행 수 저장" } + { + "type": "evaluate", + "script": "(() => { const c = document.querySelectorAll('table tbody tr').length; window.__e2e_beforeSearch = c; return 'beforeSearch=' + c; })()", + "description": "검색 전 행 수 저장" + }, + { "type": "fill", "target": "input[placeholder*='검색']", "value": "가우스", "description": "검색어 '가우스' 입력" }, + { "type": "wait", "duration": 1000, "description": "검색 결과 대기" }, + { + "type": "evaluate", + "script": "(() => { const c = document.querySelectorAll('table tbody tr').length; return 'afterSearch=' + c + ', filtered=' + (c < (window.__e2e_beforeSearch||999)); })()", + "description": "검색 후 행 수 확인" + } ], "verify": { "searchApplied": true, "tableContains": "가우스", "dataFiltered": "검색어에 맞는 거래처만 필터링되어야 함" - }, - "note": "⚠️ 검색어 입력 후 테이블에 '가우스'가 포함된 행만 표시되어야 함" + } }, { "id": 7, "name": "검색 결과 데이터 검증", "description": "검색 결과의 각 행에 검색어가 포함되어 있는지 확인", "actions": [ - { "type": "wait", "duration": 300 } + { + "type": "verify_text", + "target": "table tbody", + "text": "가우스", + "description": "테이블에 가우스 텍스트 존재 확인" + } ], "verify": { "allRowsContain": "가우스", - "rowCountChanged": "beforeSearchCount !== afterSearchCount (데이터 필터링 확인)", "verifyMethod": "테이블의 모든 행이 검색어 '가우스'를 포함하는지 확인" - }, - "note": "테이블 행 수 변화, 검색어 포함 거래처명 표시" + } }, { "id": 8, "name": "검색 초기화 및 복원 확인", "description": "검색어 삭제 후 전체 목록 복원 확인", "actions": [ - { "type": "clear", "target": "검색", "description": "검색어 삭제" }, - { "type": "wait", "duration": 500, "description": "목록 복원 대기" }, - { "type": "capture", "variable": "restoredRowCount", "selector": "table tbody tr", "extract": "count", "description": "복원 후 행 수 저장" } + { + "type": "evaluate", + "script": "(() => { const inp = document.querySelector('input[placeholder*=\"검색\"]'); if(inp){ const nset = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; nset.call(inp,''); inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true})); return 'cleared'; } return 'not found'; })()", + "description": "검색어 삭제" + }, + { "type": "wait", "duration": 1000, "description": "목록 복원 대기" }, + { + "type": "evaluate", + "script": "(() => { const c = document.querySelectorAll('table tbody tr').length; return 'restored rows=' + c + ', restored=' + (c >= (window.__e2e_beforeSearch||1)); })()", + "description": "복원 후 행 수 확인" + } ], "verify": { "dataRestored": true, - "rowCountRestored": "beforeSearchCount와 유사한 행 수로 복원" + "rowCountRestored": "검색 전과 유사한 행 수로 복원" } }, { "id": 9, - "name": "구분 필터 테스트", - "action": "select_filter", - "target": "categoryFilter", - "value": "매출", + "name": "구분 필터 테스트 (매출)", + "description": "첫 번째 Radix combobox를 클릭하여 '매출' 옵션 선택", + "actions": [ + { + "type": "evaluate", + "script": "(async () => { const cbs = document.querySelectorAll('button[role=\"combobox\"]'); if(!cbs[0]) return 'combobox not found'; cbs[0].click(); await new Promise(r=>setTimeout(r,500)); const opt = Array.from(document.querySelectorAll('[role=\"option\"]')).find(o=>o.innerText?.trim()==='매출'); if(opt){ opt.click(); await new Promise(r=>setTimeout(r,1000)); return 'selected 매출, rows=' + document.querySelectorAll('table tbody tr').length; } return 'option 매출 not found'; })()", + "description": "구분 필터에서 매출 선택" + } + ], "expected": "매출 거래처만 필터링" }, { "id": 10, "name": "구분 필터 초기화", - "action": "select_filter", - "target": "categoryFilter", - "value": "전체", + "description": "구분 필터를 '전체'로 되돌리기", + "actions": [ + { + "type": "evaluate", + "script": "(async () => { const cbs = document.querySelectorAll('button[role=\"combobox\"]'); if(!cbs[0]) return 'combobox not found'; cbs[0].click(); await new Promise(r=>setTimeout(r,500)); const opt = Array.from(document.querySelectorAll('[role=\"option\"]')).find(o=>o.innerText?.trim()==='전체'); if(opt){ opt.click(); await new Promise(r=>setTimeout(r,1000)); return 'selected 전체, rows=' + document.querySelectorAll('table tbody tr').length; } return 'option 전체 not found'; })()", + "description": "구분 필터를 전체로 초기화" + } + ], "expected": "전체 데이터 다시 표시" }, { "id": 11, "name": "테이블 행 클릭 - 상세 페이지 이동", - "action": "click_row", - "target": "first_row", + "description": "목록 페이지에서 첫 번째 행을 클릭하여 상세 페이지로 이동", + "actions": [ + { + "type": "evaluate", + "script": "(() => { const url = window.location.href; if(!url.includes('/accounting/vendors') || url.includes('mode=')) return 'NOT on list page: ' + url; return 'on list page'; })()", + "description": "목록 페이지 확인" + }, + { + "type": "evaluate", + "script": "(async () => { const rows = document.querySelectorAll('table tbody tr'); if(rows.length===0) return 'no rows'; rows[0].click(); await new Promise(r=>setTimeout(r,2000)); return 'clicked row, url=' + window.location.href; })()", + "description": "첫 번째 행 클릭" + } + ], "expected": "거래처 상세 페이지로 이동" }, { "id": 12, "name": "상세 페이지 - URL 확인", "action": "verify_url", + "target": "/accounting/vendors/\\d+", "checks": [ "URL에 거래처 ID 포함 (/accounting/vendors/{id})" ], @@ -238,7 +282,6 @@ "거래처코드 필드", "거래처명 필드", "대표자명 필드", - "거래처 유형 필드", "업태 필드", "업종 필드" ], @@ -325,6 +368,7 @@ "id": 22, "name": "수정 모드 - URL 확인", "action": "verify_url", + "target": "mode=edit", "checks": [ "URL에 mode=edit 파라미터 포함" ], @@ -347,107 +391,141 @@ { "id": 24, "name": "핵심 테스트: 거래처명 수정", - "action": "edit_field", - "target": "vendorName", - "value": " (수정테스트)", - "mode": "append", + "description": "거래처명 input 필드에 테스트 접미사 추가. input에 id/name이 없으므로 value 기반 탐색 필요", + "actions": [ + { + "type": "evaluate", + "script": "(async () => { const inputs = document.querySelectorAll('input:not([type=\"hidden\"]):not([type=\"checkbox\"])'); let target = null; inputs.forEach(inp => { if(inp.value && inp.value.length > 1 && !inp.value.includes('수정테스트') && !inp.placeholder.includes('자동생성') && !inp.placeholder.includes('000-00')) { if(!target) target = inp; } }); if(!target) { for(const inp of inputs) { if(inp.value && inp.value.length > 1 && !inp.placeholder.includes('자동생성') && !inp.placeholder.includes('000-00')) { target = inp; break; } } } if(target) { const nset = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; const origVal = target.value; nset.call(target, origVal + ' (수정테스트)'); target.dispatchEvent(new Event('input',{bubbles:true})); target.dispatchEvent(new Event('change',{bubbles:true})); window.__e2e_origVendorName = origVal; return 'modified: ' + origVal + ' → ' + target.value; } return 'no editable input found'; })()", + "description": "첫 번째 편집 가능 필드(거래처명)에 접미사 추가" + } + ], "expected": "거래처명에 ' (수정테스트)' 추가" }, { "id": 25, "name": "핵심 테스트: 저장 버튼 클릭", - "action": "click_button", - "target": "저장", - "expected": "저장 확인 다이얼로그 표시" + "description": "저장 버튼 클릭. 이 페이지는 다이얼로그 없이 직접 저장 후 목록으로 리다이렉트됨", + "actions": [ + { + "type": "evaluate", + "script": "(() => { window.__e2e_urlBeforeSave = window.location.href; return 'saved url: ' + window.__e2e_urlBeforeSave; })()", + "description": "저장 전 URL 기록" + }, + { "type": "click_button", "target": "저장", "description": "저장 버튼 클릭" }, + { "type": "wait", "duration": 2000, "description": "저장 처리 대기" } + ], + "expected": "저장 완료 후 목록 페이지로 리다이렉트" }, { "id": 26, - "name": "핵심 테스트: 저장 확인 다이얼로그", - "action": "verify_dialog", - "checks": [ - "수정 확인 타이틀", - "확인 메시지 표시", - "취소 버튼 존재", - "확인 버튼 존재" + "name": "필수 검증 #2: 저장 완료 확인", + "description": "저장 후 URL 변경 및 에러 여부 확인 (다이얼로그 없이 직접 저장 방식)", + "actions": [ + { + "type": "evaluate", + "script": "(() => { const url = window.location.href; const isListPage = url.includes('/accounting/vendors') && !url.includes('mode='); const hasError = document.body.innerText.includes('404') || document.body.innerText.includes('500') || document.body.innerText.includes('Not Found'); const urlChanged = url !== window.__e2e_urlBeforeSave; return JSON.stringify({ url, isListPage, hasError, urlChanged, result: isListPage && !hasError ? 'PASS' : 'FAIL' }); })()", + "description": "저장 후 목록 페이지 복귀 및 에러 없음 확인" + } ], - "expected": "저장 확인 다이얼로그 정상 표시" + "expected": "목록 페이지로 복귀, 에러 없음" }, { "id": 27, - "name": "필수 검증 #2: 저장 확인 버튼 클릭", - "action": "click_dialog_confirm", - "target": "확인", - "checks": [ - "URL 변경 여부 확인", - "에러 페이지 감지", - "API 호출 확인 (PUT /api/v1/clients/{id})", - "성공 토스트 (수정이 완료되었습니다)", - "상세 페이지로 복귀" + "name": "수정 결과 확인 - 목록에서 검증", + "description": "목록 페이지에서 수정된 거래처명 확인", + "actions": [ + { + "type": "evaluate", + "script": "(() => { const found = document.body.innerText.includes('수정테스트'); const rows = document.querySelectorAll('table tbody tr').length; return JSON.stringify({ modifiedVisible: found, rowCount: rows, result: found ? 'PASS: 수정된 데이터 목록에 반영' : 'WARN: 수정 텍스트 미표시 (페이지네이션 또는 정렬 영향)' }); })()", + "description": "목록에서 수정된 거래처 확인" + } ], - "expected": "수정 완료 및 상세 페이지 복귀" + "expected": "수정된 거래처명이 목록에 표시" }, { "id": 28, - "name": "수정 결과 확인", - "action": "verify_data_change", - "checks": [ - "거래처명에 수정된 값 반영", - "view 모드로 복귀 (수정 버튼 표시)" + "name": "원래 값 복원 - 수정된 거래처 클릭", + "description": "수정된 거래처를 찾아 클릭하여 상세 페이지 진입", + "actions": [ + { + "type": "evaluate", + "script": "(async () => { const rows = document.querySelectorAll('table tbody tr'); let target = null; rows.forEach(row => { if(row.innerText.includes('수정테스트')) target = row; }); if(target){ target.click(); await new Promise(r=>setTimeout(r,2000)); return 'clicked modified vendor, url=' + window.location.href; } rows[0]?.click(); await new Promise(r=>setTimeout(r,2000)); return 'modified vendor not found in current page, clicked first row. url=' + window.location.href; })()", + "description": "수정된 거래처 행 클릭" + } ], - "expected": "수정된 데이터 정상 반영" + "expected": "수정된 거래처 상세 페이지 진입" }, { "id": 29, "name": "원래 값 복원 - 수정 버튼 클릭", - "action": "click_button", - "target": "수정", + "actions": [ + { + "type": "evaluate", + "script": "(async () => { const btn = Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='수정'); if(btn){ btn.click(); await new Promise(r=>setTimeout(r,1500)); return 'edit mode, url=' + window.location.href; } return 'edit button not found'; })()", + "description": "수정 모드 진입" + } + ], "expected": "수정 모드로 전환" }, { "id": 30, - "name": "원래 값 복원 - 거래처명 수정", - "action": "edit_field", - "target": "vendorName", - "value": "", - "mode": "remove_suffix", - "suffix": " (수정테스트)", + "name": "원래 값 복원 - 거래처명 원복", + "description": "거래처명에서 ' (수정테스트)' 제거", + "actions": [ + { + "type": "evaluate", + "script": "(async () => { const inputs = document.querySelectorAll('input:not([type=\"hidden\"]):not([type=\"checkbox\"])'); let restored = false; inputs.forEach(inp => { if(inp.value.includes('수정테스트')){ const nset = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; const newVal = inp.value.replace(' (수정테스트)',''); nset.call(inp, newVal); inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true})); restored = true; } }); return restored ? 'restored' : 'no field with 수정테스트 found'; })()", + "description": "접미사 제거하여 원래 값 복원" + } + ], "expected": "거래처명에서 ' (수정테스트)' 제거" }, { "id": 31, "name": "원래 값 복원 - 저장", - "action": "click_button", - "target": "저장", - "expected": "저장 확인 다이얼로그 표시" + "description": "복원 저장 (다이얼로그 없이 직접 저장)", + "actions": [ + { + "type": "evaluate", + "script": "(async () => { const btn = Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='저장'); if(btn){ btn.click(); await new Promise(r=>setTimeout(r,2000)); return 'saved, url=' + window.location.href; } return 'save button not found'; })()", + "description": "저장 버튼 클릭" + } + ], + "expected": "원래 값으로 복원 완료, 목록으로 리다이렉트" }, { "id": 32, - "name": "원래 값 복원 - 저장 확인", - "action": "click_dialog_confirm", - "target": "확인", - "expected": "원래 값으로 복원 완료" + "name": "원래 값 복원 - 완료 확인", + "description": "복원 후 목록 페이지에서 수정테스트 텍스트 제거 확인", + "actions": [ + { + "type": "evaluate", + "script": "(() => { const url = window.location.href; const isListPage = url.includes('/accounting/vendors') && !url.includes('mode='); const stillModified = document.body.innerText.includes('수정테스트'); return JSON.stringify({ url, isListPage, stillModified, result: isListPage && !stillModified ? 'PASS: 원복 완료' : 'WARN: 원복 확인 필요' }); })()", + "description": "목록에서 원복 확인" + } + ], + "expected": "수정테스트 텍스트 제거됨" }, { "id": 33, - "name": "목록 버튼 클릭 - 목록 복귀", - "action": "click_button", - "target": "목록", - "expected": "거래처관리 목록 페이지로 복귀" + "name": "목록 페이지 최종 확인", + "action": "verify_url", + "target": "/accounting/vendors", + "expected": "거래처관리 목록 페이지 정상 표시" }, { "id": 34, - "name": "목록 페이지 복귀 확인", - "action": "verify_url", - "target": "/ko/accounting/vendors", - "expected": "목록 페이지 정상 표시" + "name": "콘솔 에러 확인", + "action": "verify_console", + "expected": "심각한 콘솔 에러 없음" } ], "requiredVerifications": [ { "id": 1, "name": "등록/저장 버튼", - "steps": [25, 26, 27], - "criteria": "저장 다이얼로그 확인 + API 호출 + 성공 토스트 + 데이터 반영" + "steps": [25, 26], + "criteria": "저장 클릭 → 목록 리다이렉트 + 에러 없음 + 데이터 반영" }, { "id": 2,