diff --git a/_global-crud-config.json b/_global-crud-config.json new file mode 100644 index 0000000..3e9b9f7 --- /dev/null +++ b/_global-crud-config.json @@ -0,0 +1,321 @@ +{ + "$schema": "E2E Global CRUD Testing Configuration", + "version": "1.0.0", + "description": "CRUD 전체 흐름 테스트를 위한 전역 설정 - 생성→조회→수정→삭제 완전 검증", + "lastUpdated": "2026-01-31", + + "crudPolicy": { + "description": "CRUD 테스트 정책", + "rules": [ + "테스트 데이터는 반드시 테스트 내에서 생성", + "기존 데이터 수정/삭제 절대 금지", + "삭제는 생성한 데이터에 대해서만 허용", + "테스트 실패 시에도 생성된 데이터 정리 시도", + "테스트 데이터는 고유 식별자로 구분" + ], + "testDataPrefix": "E2E_TEST_", + "uniqueIdentifier": "timestamp", + "cleanupOnFailure": true + }, + + "phases": { + "CREATE": { + "order": 1, + "description": "테스트 데이터 생성", + "requiredValidations": [ + "등록 폼/모달 열림 확인", + "필수 필드 입력", + "저장 버튼 클릭", + "API 호출 확인 (POST)", + "성공 토스트/메시지 확인", + "목록에 데이터 표시 확인" + ], + "expectedApi": { + "method": "POST", + "successStatus": [200, 201], + "responseValidation": "id 또는 생성된 데이터 반환" + }, + "onSuccess": "createdId 저장 → READ 단계 진행", + "onFailure": "스크린샷 + 오류 로그 → 테스트 중단" + }, + "READ": { + "order": 2, + "description": "생성된 데이터 조회 확인", + "requiredValidations": [ + "목록에서 생성된 데이터 검색", + "상세 페이지 진입", + "입력한 데이터가 정확히 표시되는지 확인", + "모든 필드 값 검증" + ], + "expectedApi": { + "method": "GET", + "successStatus": [200], + "responseValidation": "생성 시 입력한 데이터와 일치" + }, + "onSuccess": "UPDATE 단계 진행", + "onFailure": "스크린샷 + 데이터 불일치 기록 → DELETE 단계로 건너뛰기" + }, + "UPDATE": { + "order": 3, + "description": "데이터 수정", + "requiredValidations": [ + "수정 모드 진입 (URL: ?mode=edit 또는 수정 버튼)", + "필드 값 변경", + "저장 버튼 클릭", + "API 호출 확인 (PUT/PATCH)", + "성공 토스트/메시지 확인", + "변경된 데이터 표시 확인" + ], + "expectedApi": { + "method": ["PUT", "PATCH"], + "successStatus": [200], + "responseValidation": "수정된 데이터 반환" + }, + "onSuccess": "DELETE 단계 진행", + "onFailure": "스크린샷 + 오류 로그 → DELETE 단계 진행 (정리 필요)" + }, + "DELETE": { + "order": 4, + "description": "테스트 데이터 삭제 (정리)", + "requiredValidations": [ + "삭제 버튼 클릭", + "확인 다이얼로그 표시", + "삭제 확인 클릭", + "API 호출 확인 (DELETE)", + "성공 토스트/메시지 확인", + "목록에서 데이터 제거 확인" + ], + "expectedApi": { + "method": "DELETE", + "successStatus": [200, 204], + "responseValidation": "삭제 성공 응답" + }, + "onSuccess": "테스트 완료 (PASS)", + "onFailure": "수동 정리 필요 경고 + 테스트 데이터 정보 기록" + } + }, + + "testDataTemplates": { + "vendor": { + "entityName": "거래처", + "menuPath": "회계관리 > 거래처관리", + "url": "/accounting/vendors", + "fields": { + "vendorName": { "type": "text", "required": true, "testValue": "E2E_TEST_거래처_{timestamp}" }, + "businessNumber": { "type": "text", "required": false, "testValue": "123-45-67890" }, + "representative": { "type": "text", "required": false, "testValue": "테스트대표" }, + "vendorType": { "type": "select", "required": true, "testValue": "매출" }, + "phone": { "type": "text", "required": false, "testValue": "02-1234-5678" }, + "email": { "type": "email", "required": false, "testValue": "e2e@test.com" } + }, + "updateFields": { + "vendorName": "E2E_TEST_수정완료_{timestamp}", + "representative": "수정대표" + }, + "searchField": "vendorName", + "api": { + "list": "GET /api/v1/clients", + "create": "POST /api/v1/clients", + "read": "GET /api/v1/clients/{id}", + "update": "PUT /api/v1/clients/{id}", + "delete": "DELETE /api/v1/clients/{id}" + } + }, + "freeBoard": { + "entityName": "자유게시판 글", + "menuPath": "게시판 > 자유게시판", + "url": "/boards/free", + "fields": { + "title": { "type": "text", "required": true, "testValue": "E2E_TEST_게시글_{timestamp}" }, + "content": { "type": "textarea", "required": true, "testValue": "E2E 테스트용 게시글 내용입니다. 자동 생성됨." } + }, + "updateFields": { + "title": "E2E_TEST_수정됨_{timestamp}", + "content": "수정된 게시글 내용입니다." + }, + "searchField": "title", + "api": { + "list": "GET /api/v1/boards/free", + "create": "POST /api/v1/boards/free", + "read": "GET /api/v1/boards/free/{id}", + "update": "PUT /api/v1/boards/free/{id}", + "delete": "DELETE /api/v1/boards/free/{id}" + } + }, + "deposit": { + "entityName": "입금", + "menuPath": "회계관리 > 입금관리", + "url": "/accounting/deposits", + "fields": { + "vendorName": { "type": "select", "required": true, "testValue": "가우스전자" }, + "amount": { "type": "number", "required": true, "testValue": "100000" }, + "depositDate": { "type": "date", "required": true, "testValue": "today" }, + "description": { "type": "text", "required": false, "testValue": "E2E_TEST_입금_{timestamp}" } + }, + "updateFields": { + "amount": "200000", + "description": "E2E_TEST_수정입금_{timestamp}" + }, + "searchField": "description" + } + }, + + "scripts": { + "generateTimestamp": "(function() { const now = new Date(); const pad = n => n.toString().padStart(2, '0'); return `${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; })()", + + "generateTestData": "(function(template, timestamp) { const result = {}; for (const [key, config] of Object.entries(template.fields)) { let value = config.testValue; if (typeof value === 'string' && value.includes('{timestamp}')) { value = value.replace('{timestamp}', timestamp); } if (value === 'today') { value = new Date().toISOString().split('T')[0]; } result[key] = value; } return result; })", + + "findTestDataInTable": "(function(searchText) { const rows = document.querySelectorAll('table tbody tr'); for (const row of rows) { if (row.innerText.includes(searchText)) { return { found: true, row: row, text: row.innerText.substring(0, 100) }; } } return { found: false }; })", + + "verifyDataCreated": "(async function(searchText, timeout = 5000) { const start = Date.now(); while (Date.now() - start < timeout) { const result = findTestDataInTable(searchText); if (result.found) return result; await new Promise(r => setTimeout(r, 500)); } return { found: false, error: 'timeout' }; })", + + "clickCreateButton": "(function() { const buttons = Array.from(document.querySelectorAll('button')); const createBtn = buttons.find(b => ['등록', '추가', '신규', '작성'].some(t => b.innerText?.includes(t))); if (createBtn) { createBtn.click(); return { clicked: true, text: createBtn.innerText }; } return { clicked: false, error: 'Create button not found' }; })", + + "clickEditButton": "(function() { const buttons = Array.from(document.querySelectorAll('button')); const editBtn = buttons.find(b => ['수정', '편집', 'Edit'].some(t => b.innerText?.includes(t))); if (editBtn) { editBtn.click(); return { clicked: true, text: editBtn.innerText }; } return { clicked: false, error: 'Edit button not found' }; })", + + "clickDeleteButton": "(function() { const buttons = Array.from(document.querySelectorAll('button')); const deleteBtn = buttons.find(b => ['삭제', '제거', 'Delete'].some(t => b.innerText?.includes(t))); if (deleteBtn) { deleteBtn.click(); return { clicked: true, text: deleteBtn.innerText }; } return { clicked: false, error: 'Delete button not found' }; })", + + "clickSaveButton": "(function() { const buttons = Array.from(document.querySelectorAll('button')); const saveBtn = buttons.find(b => ['저장', '등록', '확인', 'Save', 'Submit'].some(t => b.innerText?.trim() === t || b.innerText?.includes(t))); if (saveBtn) { saveBtn.click(); return { clicked: true, text: saveBtn.innerText }; } return { clicked: false, error: 'Save button not found' }; })", + + "clickConfirmDialog": "(async function() { await new Promise(r => setTimeout(r, 500)); const dialog = document.querySelector('[role=\"alertdialog\"], [role=\"dialog\"], [class*=\"modal\"]'); if (dialog) { const confirmBtn = Array.from(dialog.querySelectorAll('button')).find(b => ['확인', '예', 'Yes', 'OK', '삭제'].some(t => b.innerText?.includes(t))); if (confirmBtn) { confirmBtn.click(); return { clicked: true, text: confirmBtn.innerText }; } } return { clicked: false, error: 'Confirm button not found' }; })", + + "fillField": "(function(labelOrName, value) { const inputs = document.querySelectorAll('input, textarea, select'); for (const input of inputs) { const label = input.closest('label')?.innerText || input.placeholder || input.name || input.id; if (label?.includes(labelOrName)) { if (input.tagName === 'SELECT') { const option = Array.from(input.options).find(o => o.text.includes(value)); if (option) { input.value = option.value; input.dispatchEvent(new Event('change', { bubbles: true })); return { filled: true, field: labelOrName }; } } else { input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); return { filled: true, field: labelOrName }; } } } return { filled: false, error: `Field '${labelOrName}' not found` }; })", + + "getFieldValue": "(function(labelOrName) { const inputs = document.querySelectorAll('input, textarea, select'); for (const input of inputs) { const label = input.closest('label')?.innerText || input.placeholder || input.name || input.id; if (label?.includes(labelOrName)) { return { found: true, field: labelOrName, value: input.value }; } } return { found: false, error: `Field '${labelOrName}' not found` }; })", + + "waitForToast": "(async function(expectedText, timeout = 5000) { const start = Date.now(); while (Date.now() - start < timeout) { const toasts = document.querySelectorAll('[class*=\"toast\"], [class*=\"Toast\"], [class*=\"notification\"], [class*=\"alert\"]:not([role=\"alertdialog\"])'); for (const toast of toasts) { if (toast.innerText?.includes(expectedText)) { return { found: true, text: toast.innerText }; } } await new Promise(r => setTimeout(r, 200)); } return { found: false, error: 'Toast not found', expected: expectedText }; })", + + "captureTestContext": "(function() { return { url: window.location.href, timestamp: new Date().toISOString(), pageTitle: document.title, apiLogs: window.__API_LOGS__ || [], apiErrors: window.__API_ERRORS__ || [] }; })" + }, + + "flowTemplates": { + "standardCRUD": { + "description": "표준 CRUD 흐름 템플릿", + "steps": [ + { + "phase": "SETUP", + "name": "테스트 준비", + "actions": [ + { "type": "generateTimestamp", "saveAs": "testTimestamp" }, + { "type": "initApiMonitoring" }, + { "type": "navigateToPage" } + ] + }, + { + "phase": "CREATE", + "name": "데이터 생성", + "actions": [ + { "type": "clickCreateButton" }, + { "type": "waitForModal", "timeout": 2000 }, + { "type": "fillTestData", "template": "{{entityTemplate}}" }, + { "type": "clickSaveButton" }, + { "type": "waitForToast", "contains": ["등록", "완료", "성공"] }, + { "type": "verifyApiCall", "method": "POST", "status": [200, 201] }, + { "type": "closeModalIfOpen" }, + { "type": "searchTestData" }, + { "type": "verifyDataInTable" } + ], + "saveResult": "createdData" + }, + { + "phase": "READ", + "name": "데이터 조회", + "actions": [ + { "type": "clickTestDataRow" }, + { "type": "waitForDetailPage" }, + { "type": "verifyFieldValues", "expected": "{{createdData}}" }, + { "type": "verifyApiCall", "method": "GET", "status": [200] } + ] + }, + { + "phase": "UPDATE", + "name": "데이터 수정", + "actions": [ + { "type": "clickEditButton" }, + { "type": "waitForEditMode" }, + { "type": "updateTestData", "template": "{{updateTemplate}}" }, + { "type": "clickSaveButton" }, + { "type": "waitForToast", "contains": ["수정", "완료", "성공"] }, + { "type": "verifyApiCall", "method": ["PUT", "PATCH"], "status": [200] }, + { "type": "verifyUpdatedValues" } + ], + "saveResult": "updatedData" + }, + { + "phase": "DELETE", + "name": "데이터 삭제", + "actions": [ + { "type": "clickDeleteButton" }, + { "type": "waitForConfirmDialog" }, + { "type": "clickConfirmDialog" }, + { "type": "waitForToast", "contains": ["삭제", "완료", "성공"] }, + { "type": "verifyApiCall", "method": "DELETE", "status": [200, 204] }, + { "type": "verifyRedirectToList" }, + { "type": "verifyDataNotInTable" } + ] + }, + { + "phase": "CLEANUP", + "name": "정리", + "actions": [ + { "type": "clearSearchFilter" }, + { "type": "captureApiSummary" }, + { "type": "generateReport" } + ] + } + ] + } + }, + + "errorHandling": { + "onCreateFailure": { + "actions": ["screenshot", "logError", "skipToCleanup"], + "testResult": "FAIL", + "reason": "CREATE 단계 실패 - 데이터 생성 불가" + }, + "onReadFailure": { + "actions": ["screenshot", "logError", "continueToDelete"], + "testResult": "PARTIAL", + "reason": "READ 단계 실패 - 데이터 조회 불일치" + }, + "onUpdateFailure": { + "actions": ["screenshot", "logError", "continueToDelete"], + "testResult": "PARTIAL", + "reason": "UPDATE 단계 실패 - 수정 저장 실패" + }, + "onDeleteFailure": { + "actions": ["screenshot", "logError", "recordManualCleanup"], + "testResult": "FAIL", + "reason": "DELETE 단계 실패 - 수동 정리 필요", + "manualCleanupRequired": true + } + }, + + "reportTemplate": { + "sections": [ + { + "name": "테스트 요약", + "fields": ["시나리오명", "실행시간", "전체결과", "소요시간"] + }, + { + "name": "CRUD 단계별 결과", + "fields": ["CREATE", "READ", "UPDATE", "DELETE"], + "format": "table" + }, + { + "name": "API 호출 검증", + "fields": ["총 호출수", "성공", "실패", "응답시간"], + "includeDetails": true + }, + { + "name": "테스트 데이터", + "fields": ["생성 데이터", "수정 데이터", "삭제 확인"] + }, + { + "name": "오류 및 경고", + "conditional": "hasErrors", + "fields": ["오류 메시지", "스크린샷", "수동 정리 필요 여부"] + } + ] + } +}