diff --git a/attendance-checkin.json b/attendance-checkin.json index 1169fa2..1aaf604 100644 --- a/attendance-checkin.json +++ b/attendance-checkin.json @@ -92,53 +92,46 @@ "steps": [ { "id": "step-0", - "name": "πŸ” μœ„μΉ˜ κΆŒν•œ 사전 μ„€μ • (Playwright)", - "description": "Playwright μ»¨ν…μŠ€νŠΈ λ ˆλ²¨μ—μ„œ μœ„μΉ˜ κΆŒν•œμ„ 미리 ν—ˆμš©ν•˜μ—¬ λΈŒλΌμš°μ € νŒμ—… λ°©μ§€", + "name": "πŸ” Geolocation API λͺ¨ν‚Ή (κΆŒν•œ νŒμ—… λ°©μ§€)", + "description": "νŽ˜μ΄μ§€ λ‘œλ“œ 직후 Geolocation APIλ₯Ό λͺ¨ν‚Ήν•˜μ—¬ λΈŒλΌμš°μ € κΆŒν•œ νŒμ—…μ΄ λ‚˜νƒ€λ‚˜μ§€ μ•Šλ„λ‘ 함", "critical": true, - "executeBeforeNavigation": true, - "playwright": { - "commands": [ - { - "tool": "mcp__playwright__playwright_evaluate", - "script": "// λΈŒλΌμš°μ € μ‹œμž‘ μ‹œ μœ„μΉ˜ κΆŒν•œ μžλ™ ν—ˆμš© (Playwright μ»¨ν…μŠ€νŠΈ 레벨)" - } - ], - "contextSetup": { - "permissions": ["geolocation"], - "geolocation": { - "latitude": 37.557358, - "longitude": 126.864414 - } - } - }, - "note": "이 λ‹¨κ³„λŠ” λΈŒλΌμš°μ € μ»¨ν…μŠ€νŠΈ 생성 μ‹œ μžλ™μœΌλ‘œ 처리됨" + "executeBeforeNavigation": false, + "executeImmediately": true, + "actions": [ + { + "type": "evaluate", + "script": "(() => { const mockPosition = { coords: { latitude: 37.557358, longitude: 126.864414, accuracy: 100, altitude: null, altitudeAccuracy: null, heading: null, speed: null }, timestamp: Date.now() }; const mockGeolocation = { getCurrentPosition: (success, error, options) => { console.log('[E2E] Geolocation.getCurrentPosition - λͺ¨ν‚Ήλœ μœ„μΉ˜ λ°˜ν™˜'); setTimeout(() => success(mockPosition), 50); }, watchPosition: (success, error, options) => { console.log('[E2E] Geolocation.watchPosition - λͺ¨ν‚Ήλœ μœ„μΉ˜ λ°˜ν™˜'); setTimeout(() => success(mockPosition), 50); return 1; }, clearWatch: (id) => {} }; Object.defineProperty(navigator, 'geolocation', { value: mockGeolocation, writable: false, configurable: true }); console.log('[E2E] Geolocation API λͺ¨ν‚Ή μ™„λ£Œ - μ„œμšΈ μ˜λ“±ν¬κ΅¬ μ’Œν‘œ'); return { success: true, coords: mockPosition.coords }; })()", + "description": "Geolocation API λͺ¨ν‚Ή (μ„œμšΈ μ˜λ“±ν¬κ΅¬ μ’Œν‘œ: 37.557358, 126.864414)" + }, + { "type": "wait", "duration": 300, "description": "λͺ¨ν‚Ή 적용 λŒ€κΈ°" } + ], + "note": "Geolocation APIλ₯Ό λͺ¨ν‚Ήν•˜λ©΄ λΈŒλΌμš°μ €κ°€ μœ„μΉ˜ κΆŒν•œμ„ μš”μ²­ν•˜μ§€ μ•ŠμŒ" }, { "id": "step-0-1", - "name": "πŸ—ΊοΈ μœ„μΉ˜ κΆŒν•œ νŒμ—… 처리 및 μ‚¬μ΄λ“œλ°” 메뉴 전체 펼치기", - "description": "μœ„μΉ˜ κΆŒν•œ μš”μ²­ νŒμ—… 처리 ν›„ λͺ¨λ‘ 펼치기 λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ 전체 메뉴 펼침", + "name": "πŸ—ΊοΈ λΈŒλΌμš°μ € μœ„μΉ˜ κΆŒν•œ νŒμ—… 클릭 (쒌츑 상단)", + "description": "Chrome λΈŒλΌμš°μ € 쒌츑 상단에 λ‚˜νƒ€λ‚˜λŠ” 'μ‚¬μ΄νŠΈμ— μžˆλŠ” λ™μ•ˆ ν—ˆμš©' νŒμ—… 클릭", "critical": true, "actions": [ - { "type": "wait", "duration": 1000, "description": "νŽ˜μ΄μ§€ λ‘œλ“œ 및 κΆŒν•œ νŒμ—… ν‘œμ‹œ λŒ€κΈ°" }, + { "type": "wait", "duration": 1500, "description": "μœ„μΉ˜ κΆŒν•œ νŒμ—… ν‘œμ‹œ λŒ€κΈ°" }, { - "type": "conditionalClick", - "description": "μœ„μΉ˜ κΆŒν•œ νŒμ—…μ΄ λ‚˜νƒ€λ‚˜λ©΄ 'ν—ˆμš©' λ˜λŠ” '항상 ν—ˆμš©' λ²„νŠΌ 클릭", - "selectors": [ - "button:has-text('항상 ν—ˆμš©')", - "button:has-text('ν—ˆμš©')", - "button:has-text('Allow')", - "button:has-text('Always Allow')", - "[data-testid='allow-location']", - ".permission-allow-button", - "button[aria-label*='ν—ˆμš©']", - "button[aria-label*='μœ„μΉ˜']" - ], - "fallback": { - "action": "skip", - "reason": "κΆŒν•œ νŒμ—…μ΄ μ—†μœΌλ©΄ 이미 ν—ˆμš©λœ μƒνƒœ" - } + "type": "evaluate", + "script": "(async function() { const permissionSelectors = [ '[class*=\"permission\"][class*=\"allow\"]', '[class*=\"infobar\"] button', '[aria-label*=\"ν—ˆμš©\"]', '[aria-label*=\"Allow\"]', 'button:has-text(\"μ‚¬μ΄νŠΈμ— μžˆλŠ” λ™μ•ˆ ν—ˆμš©\")', 'button:has-text(\"ν—ˆμš©\")', 'button:has-text(\"Allow\")', '[data-testid*=\"permission\"]', '.permission-prompt button', '[class*=\"PermissionPrompt\"] button' ]; for (const sel of permissionSelectors) { try { const btn = document.querySelector(sel); if (btn && btn.offsetParent !== null) { btn.click(); console.log('[E2E] μœ„μΉ˜ κΆŒν•œ νŒμ—… 클릭 성곡:', sel); await new Promise(r => setTimeout(r, 500)); return { clicked: true, selector: sel }; } } catch(e) {} } const allButtons = Array.from(document.querySelectorAll('button, [role=\"button\"]')); const allowBtn = allButtons.find(b => { const text = b.innerText || b.textContent || ''; return text.includes('μ‚¬μ΄νŠΈμ— μžˆλŠ” λ™μ•ˆ ν—ˆμš©') || text.includes('ν—ˆμš©') || text.includes('Allow'); }); if (allowBtn && allowBtn.offsetParent !== null) { allowBtn.click(); console.log('[E2E] μœ„μΉ˜ κΆŒν•œ νŒμ—… ν…μŠ€νŠΈ κ²€μƒ‰μœΌλ‘œ 클릭'); return { clicked: true, method: 'textSearch' }; } console.log('[E2E] μœ„μΉ˜ κΆŒν•œ νŒμ—… μ—†μŒ (이미 ν—ˆμš©λ˜μ—ˆκ±°λ‚˜ λͺ¨ν‚ΉμœΌλ‘œ 우회됨)'); return { clicked: false, reason: 'no_popup_found' }; })()", + "description": "쒌츑 상단 κΆŒν•œ νŒμ—… μ°Ύμ•„μ„œ 클릭" }, - { "type": "wait", "duration": 500, "description": "κΆŒν•œ μ„€μ • 적용 λŒ€κΈ°" }, + { "type": "wait", "duration": 500, "description": "κΆŒν•œ μ„€μ • 적용 λŒ€κΈ°" } + ], + "errorHandling": { + "onTimeout": "continue", + "onNotFound": "continue", + "reason": "νŒμ—…μ΄ μ—†μœΌλ©΄ 이미 ν—ˆμš©λœ μƒνƒœλ‘œ κ°„μ£Ό" + } + }, + { + "id": "step-0-2", + "name": "πŸ“‚ μ‚¬μ΄λ“œλ°” 메뉴 전체 펼치기", + "description": "λͺ¨λ‘ 펼치기 λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ 전체 메뉴 펼침", + "actions": [ { "type": "evaluate", "script": "document.querySelector('.sidebar-scroll')?.scrollTo({top:0,behavior:'instant'})" @@ -152,14 +145,8 @@ { "type": "screenshot", "name": "after_permission_grant_and_menu_expanded" } ], "verification": [ - "μœ„μΉ˜ κΆŒν•œ νŒμ—…μ΄ λ‹«ν˜”λŠ”μ§€ 확인", - "λ˜λŠ” νŒμ—…μ΄ μ²˜μŒλΆ€ν„° μ—†μ—ˆλŠ”μ§€ 확인 (이미 ν—ˆμš©λœ 경우)" - ], - "errorHandling": { - "onTimeout": "continue", - "onNotFound": "continue", - "reason": "νŒμ—…μ΄ μ—†μœΌλ©΄ κΆŒν•œμ΄ 이미 ν—ˆμš©λœ μƒνƒœλ‘œ κ°„μ£Ό" - } + "μ‚¬μ΄λ“œλ°” 메뉴가 νŽΌμ³μ‘ŒλŠ”μ§€ 확인" + ] }, { "id": 2, diff --git a/attendance-management.json b/attendance-management.json index 78a9b39..7262b1e 100644 --- a/attendance-management.json +++ b/attendance-management.json @@ -81,7 +81,42 @@ "steps": [ { "id": "step-0", - "name": "μ‚¬μ΄λ“œλ°” 메뉴 전체 펼치기", + "name": "πŸ” Geolocation API λͺ¨ν‚Ή (κΆŒν•œ νŒμ—… λ°©μ§€)", + "description": "νŽ˜μ΄μ§€ λ‘œλ“œ 직후 Geolocation APIλ₯Ό λͺ¨ν‚Ήν•˜μ—¬ λΈŒλΌμš°μ € κΆŒν•œ νŒμ—…μ΄ λ‚˜νƒ€λ‚˜μ§€ μ•Šλ„λ‘ 함", + "critical": true, + "actions": [ + { + "type": "evaluate", + "script": "(() => { const mockPosition = { coords: { latitude: 37.5665, longitude: 126.9780, accuracy: 100, altitude: null, altitudeAccuracy: null, heading: null, speed: null }, timestamp: Date.now() }; const mockGeolocation = { getCurrentPosition: (success, error, options) => { console.log('[E2E] Geolocation.getCurrentPosition - λͺ¨ν‚Ήλœ μœ„μΉ˜ λ°˜ν™˜'); setTimeout(() => success(mockPosition), 50); }, watchPosition: (success, error, options) => { console.log('[E2E] Geolocation.watchPosition - λͺ¨ν‚Ήλœ μœ„μΉ˜ λ°˜ν™˜'); setTimeout(() => success(mockPosition), 50); return 1; }, clearWatch: (id) => {} }; Object.defineProperty(navigator, 'geolocation', { value: mockGeolocation, writable: false, configurable: true }); console.log('[E2E] Geolocation API λͺ¨ν‚Ή μ™„λ£Œ - μ„œμšΈμ‹œμ²­ μ’Œν‘œ'); return { success: true, coords: mockPosition.coords }; })()", + "description": "Geolocation API λͺ¨ν‚Ή (μ„œμšΈμ‹œμ²­ μ’Œν‘œ: 37.5665, 126.9780)" + }, + { "type": "wait", "duration": 300, "description": "λͺ¨ν‚Ή 적용 λŒ€κΈ°" } + ], + "note": "Geolocation APIλ₯Ό λͺ¨ν‚Ήν•˜λ©΄ λΈŒλΌμš°μ €κ°€ μœ„μΉ˜ κΆŒν•œμ„ μš”μ²­ν•˜μ§€ μ•ŠμŒ" + }, + { + "id": "step-0-1", + "name": "πŸ—ΊοΈ λΈŒλΌμš°μ € μœ„μΉ˜ κΆŒν•œ νŒμ—… 클릭 (쒌츑 상단)", + "description": "Chrome λΈŒλΌμš°μ € 쒌츑 상단에 λ‚˜νƒ€λ‚˜λŠ” 'μ‚¬μ΄νŠΈμ— μžˆλŠ” λ™μ•ˆ ν—ˆμš©' νŒμ—… 클릭", + "critical": true, + "actions": [ + { "type": "wait", "duration": 1500, "description": "μœ„μΉ˜ κΆŒν•œ νŒμ—… ν‘œμ‹œ λŒ€κΈ°" }, + { + "type": "evaluate", + "script": "(async function() { const permissionSelectors = [ '[class*=\"permission\"][class*=\"allow\"]', '[class*=\"infobar\"] button', '[aria-label*=\"ν—ˆμš©\"]', '[aria-label*=\"Allow\"]', 'button:has-text(\"μ‚¬μ΄νŠΈμ— μžˆλŠ” λ™μ•ˆ ν—ˆμš©\")', 'button:has-text(\"ν—ˆμš©\")', 'button:has-text(\"Allow\")', '[data-testid*=\"permission\"]', '.permission-prompt button', '[class*=\"PermissionPrompt\"] button' ]; for (const sel of permissionSelectors) { try { const btn = document.querySelector(sel); if (btn && btn.offsetParent !== null) { btn.click(); console.log('[E2E] μœ„μΉ˜ κΆŒν•œ νŒμ—… 클릭 성곡:', sel); await new Promise(r => setTimeout(r, 500)); return { clicked: true, selector: sel }; } } catch(e) {} } const allButtons = Array.from(document.querySelectorAll('button, [role=\"button\"]')); const allowBtn = allButtons.find(b => { const text = b.innerText || b.textContent || ''; return text.includes('μ‚¬μ΄νŠΈμ— μžˆλŠ” λ™μ•ˆ ν—ˆμš©') || text.includes('ν—ˆμš©') || text.includes('Allow'); }); if (allowBtn && allowBtn.offsetParent !== null) { allowBtn.click(); console.log('[E2E] μœ„μΉ˜ κΆŒν•œ νŒμ—… ν…μŠ€νŠΈ κ²€μƒ‰μœΌλ‘œ 클릭'); return { clicked: true, method: 'textSearch' }; } console.log('[E2E] μœ„μΉ˜ κΆŒν•œ νŒμ—… μ—†μŒ (이미 ν—ˆμš©λ˜μ—ˆκ±°λ‚˜ λͺ¨ν‚ΉμœΌλ‘œ 우회됨)'); return { clicked: false, reason: 'no_popup_found' }; })()", + "description": "쒌츑 상단 κΆŒν•œ νŒμ—… μ°Ύμ•„μ„œ 클릭" + }, + { "type": "wait", "duration": 500, "description": "κΆŒν•œ μ„€μ • 적용 λŒ€κΈ°" } + ], + "errorHandling": { + "onTimeout": "continue", + "onNotFound": "continue", + "reason": "νŒμ—…μ΄ μ—†μœΌλ©΄ 이미 ν—ˆμš©λœ μƒνƒœλ‘œ κ°„μ£Ό" + } + }, + { + "id": "step-0-2", + "name": "πŸ“‚ μ‚¬μ΄λ“œλ°” 메뉴 전체 펼치기", "description": "λͺ¨λ‘ 펼치기 λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ 전체 메뉴λ₯Ό 펼친 ν›„ 메뉴 탐색 μ€€λΉ„", "actions": [ {