{ "$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": ["오류 메시지", "스크린샷", "수동 정리 필요 여부"] } ] } }