diff --git a/e2e/results/hotfix/Download-Debug_2026-03-04_20-21-21.md b/e2e/results/hotfix/Download-Debug_2026-03-04_20-21-21.md new file mode 100644 index 0000000..805f8b9 --- /dev/null +++ b/e2e/results/hotfix/Download-Debug_2026-03-04_20-21-21.md @@ -0,0 +1,54 @@ +# πŸ” λ‹€μš΄λ‘œλ“œ 디버깅 상세 리포트 +**μ‹€ν–‰ μ‹œκ°„**: 2026-03-04_20-21-21 +## μΉ΄ν…Œκ³ λ¦¬λ³„ κ²°κ³Ό + +### ❌ λ²„νŠΌ μžˆμœΌλ‚˜ λ‹€μš΄λ‘œλ“œ λ―Έλ™μž‘ +- **νšŒκ³„κ΄€λ¦¬ > 일일 일보**: "μ—‘μ…€" + - λ„€νŠΈμ›Œν¬ 응닡: 404 https://dev.codebridge-x.com/api/proxy/daily-report/export?date=2026-03-04 +- **νšŒκ³„κ΄€λ¦¬ > κ³„μ’Œμž…μΆœκΈˆλ‚΄μ—­**: "μ—‘μ…€ λ‹€μš΄λ‘œλ“œ" + - λ„€νŠΈμ›Œν¬ 응닡: 200 https://dev.codebridge-x.com/accounting/bank-transactions + - Server Actions: https://dev.codebridge-x.com/accounting/bank-transactions +- **νšŒκ³„κ΄€λ¦¬ > μΉ΄λ“œμ‚¬μš©λ‚΄μ—­**: "μ—‘μ…€ λ‹€μš΄λ‘œλ“œ" +- **νšŒκ³„κ΄€λ¦¬ > μ„ΈκΈˆκ³„μ‚°μ„œκ΄€λ¦¬**: "μ—‘μ…€ λ‹€μš΄λ‘œλ“œ" + - λ„€νŠΈμ›Œν¬ 응닡: 200 https://dev.codebridge-x.com/accounting/tax-invoices + - Server Actions: https://dev.codebridge-x.com/accounting/tax-invoices +- **νšŒκ³„κ΄€λ¦¬ > λ―Έμˆ˜κΈˆν˜„ν™©**: "μ—‘μ…€ λ‹€μš΄λ‘œλ“œ" + - λ„€νŠΈμ›Œν¬ 응닡: 200 https://dev.codebridge-x.com/accounting/receivables-status + - Server Actions: https://dev.codebridge-x.com/accounting/receivables-status +- **νšŒκ³„κ΄€λ¦¬ > κ±°λž˜μ²˜μ›μž₯**: "μ—‘μ…€ λ‹€μš΄λ‘œλ“œ" + - λ„€νŠΈμ›Œν¬ 응닡: 200 https://dev.codebridge-x.com/accounting/vendor-ledger + - Server Actions: https://dev.codebridge-x.com/accounting/vendor-ledger +- **μžμž¬κ΄€λ¦¬ > μž¬κ³ ν˜„ν™©**: "μ—‘μ…€ λ‹€μš΄λ‘œλ“œ" + - λ„€νŠΈμ›Œν¬ 응닡: 200 https://dev.codebridge-x.com/api/proxy/stocks?start_date=2026-03-01&end_date=2026-03-04&size=1000&per_page=1000&page=1 + +### ⏭️ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ +- **νšŒκ³„κ΄€λ¦¬ > λ§€μž…κ΄€λ¦¬**: μ£Όμš” λ²„νŠΌ: "홍킬동 + +κ°œλ°œμ€‘μΈ 메뉴", "λͺ¨λ‘ μ ‘κΈ°", "μ‹œμŠ€ν…œ λŒ€μ‹œλ³΄λ“œ", "ν’ˆμ§ˆκ΄€λ¦¬", "ν’ˆλͺ©κ΄€λ¦¬", "κ²°μž¬κ΄€λ¦¬", "기쀀정보 관리", "κ²Œμ‹œνŒ" +- **νšŒκ³„κ΄€λ¦¬ > μ§€μΆœμ˜ˆμƒλ‚΄μ—­μ„œ**: μ£Όμš” λ²„νŠΌ: "홍킬동 + +κ°œλ°œμ€‘μΈ 메뉴", "λͺ¨λ‘ μ ‘κΈ°", "μ‹œμŠ€ν…œ λŒ€μ‹œλ³΄λ“œ", "ν’ˆμ§ˆκ΄€λ¦¬", "ν’ˆλͺ©κ΄€λ¦¬", "κ²°μž¬κ΄€λ¦¬", "기쀀정보 관리", "κ²Œμ‹œνŒ" +- **νšŒκ³„κ΄€λ¦¬ > κ²°μ œλ‚΄μ—­**: μ£Όμš” λ²„νŠΌ: "홍킬동 + +κ°œλ°œμ€‘μΈ 메뉴", "λͺ¨λ‘ μ ‘κΈ°", "μ‹œμŠ€ν…œ λŒ€μ‹œλ³΄λ“œ", "ν’ˆμ§ˆκ΄€λ¦¬", "ν’ˆλͺ©κ΄€λ¦¬", "κ²°μž¬κ΄€λ¦¬", "기쀀정보 관리", "κ²Œμ‹œνŒ" +- **νšŒκ³„κ΄€λ¦¬ > λ§€μΆœκ΄€λ¦¬**: μ£Όμš” λ²„νŠΌ: "홍킬동 + +κ°œλ°œμ€‘μΈ 메뉴", "λͺ¨λ‘ μ ‘κΈ°", "μ‹œμŠ€ν…œ λŒ€μ‹œλ³΄λ“œ", "ν’ˆμ§ˆκ΄€λ¦¬", "ν’ˆλͺ©κ΄€λ¦¬", "κ²°μž¬κ΄€λ¦¬", "기쀀정보 관리", "κ²Œμ‹œνŒ" +- **νšŒκ³„κ΄€λ¦¬ > μΆœκΈˆκ΄€λ¦¬**: μ£Όμš” λ²„νŠΌ: "홍킬동 + +κ°œλ°œμ€‘μΈ 메뉴", "λͺ¨λ‘ μ ‘κΈ°", "μ‹œμŠ€ν…œ λŒ€μ‹œλ³΄λ“œ", "ν’ˆμ§ˆκ΄€λ¦¬", "ν’ˆλͺ©κ΄€λ¦¬", "κ²°μž¬κ΄€λ¦¬", "기쀀정보 관리", "κ²Œμ‹œνŒ" +- **인사관리 > κ·Όνƒœν˜„ν™©**: μ£Όμš” λ²„νŠΌ: "홍킬동 + +κ°œλ°œμ€‘μΈ 메뉴", "λͺ¨λ‘ μ ‘κΈ°", "μ‹œμŠ€ν…œ λŒ€μ‹œλ³΄λ“œ", "ν’ˆμ§ˆκ΄€λ¦¬", "ν’ˆλͺ©κ΄€λ¦¬", "κ²°μž¬κ΄€λ¦¬", "기쀀정보 관리", "κ²Œμ‹œνŒ" +- **ꡬ맀관리 > κ΅¬λ§€ν˜„ν™©**: μ£Όμš” λ²„νŠΌ: "홍킬동 + +κ°œλ°œμ€‘μΈ 메뉴", "λͺ¨λ‘ μ ‘κΈ°", "μ‹œμŠ€ν…œ λŒ€μ‹œλ³΄λ“œ", "ν’ˆμ§ˆκ΄€λ¦¬", "ν’ˆλͺ©κ΄€λ¦¬", "κ²°μž¬κ΄€λ¦¬", "기쀀정보 관리", "κ²Œμ‹œνŒ" +- **νŒλ§€κ΄€λ¦¬ > 단가관리**: μ£Όμš” λ²„νŠΌ: "홍킬동 + +κ°œλ°œμ€‘μΈ 메뉴", "λͺ¨λ‘ μ ‘κΈ°", "μ‹œμŠ€ν…œ λŒ€μ‹œλ³΄λ“œ", "ν’ˆμ§ˆκ΄€λ¦¬", "ν’ˆλͺ©κ΄€λ¦¬", "κ²°μž¬κ΄€λ¦¬", "기쀀정보 관리", "κ²Œμ‹œνŒ" +- **νŒλ§€κ΄€λ¦¬ > κ±°λž˜μ²˜κ΄€λ¦¬**: μ£Όμš” λ²„νŠΌ: "홍킬동 + +κ°œλ°œμ€‘μΈ 메뉴", "λͺ¨λ‘ μ ‘κΈ°", "μ‹œμŠ€ν…œ λŒ€μ‹œλ³΄λ“œ", "ν’ˆμ§ˆκ΄€λ¦¬", "ν’ˆλͺ©κ΄€λ¦¬", "κ²°μž¬κ΄€λ¦¬", "기쀀정보 관리", "κ²Œμ‹œνŒ" +- **νŒλ§€κ΄€λ¦¬ > 견적관리**: μ£Όμš” λ²„νŠΌ: "홍킬동 + +κ°œλ°œμ€‘μΈ 메뉴", "λͺ¨λ‘ μ ‘κΈ°", "μ‹œμŠ€ν…œ λŒ€μ‹œλ³΄λ“œ", "ν’ˆμ§ˆκ΄€λ¦¬", "ν’ˆλͺ©κ΄€λ¦¬", "κ²°μž¬κ΄€λ¦¬", "기쀀정보 관리", "κ²Œμ‹œνŒ" diff --git a/e2e/results/hotfix/Download-Verify_2026-03-04_20-16-16.md b/e2e/results/hotfix/Download-Verify_2026-03-04_20-16-16.md new file mode 100644 index 0000000..6165ffb --- /dev/null +++ b/e2e/results/hotfix/Download-Verify_2026-03-04_20-16-16.md @@ -0,0 +1,131 @@ +# πŸ“₯ λ‹€μš΄λ‘œλ“œ κΈ°λŠ₯ 검증 리포트 + +**μ‹€ν–‰ μ‹œκ°„**: 2026-03-04 20-16-16 | **μ†Œμš”**: 245.5초 + +## πŸ“Š μš”μ•½ + +| ν•­λͺ© | κ²°κ³Ό | +|------|------| +| 전체 νŽ˜μ΄μ§€ | 20개 | +| βœ… PASS | 1개 | +| ⚠️ PARTIAL | 0개 | +| ❌ FAIL | 9개 | +| ⏭️ SKIP/ERROR | 10개 | +| λ‹€μš΄λ‘œλ“œ μ‹œλ„ | 10건 | +| λ‹€μš΄λ‘œλ“œ 성곡 | 1건 | +| λ‹€μš΄λ‘œλ“œ μ‹€νŒ¨ | 9건 | + +## πŸ“‹ νŽ˜μ΄μ§€λ³„ κ²°κ³Ό + +| # | νŽ˜μ΄μ§€ | μƒνƒœ | λ‹€μš΄λ‘œλ“œ | λΉ„κ³  | +|---|--------|------|---------|------| +| 1 | νšŒκ³„κ΄€λ¦¬ > λ§€μž…κ΄€λ¦¬ | ⏭️ NO_BUTTON | - | λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€) | +| 2 | νšŒκ³„κ΄€λ¦¬ > 일일 일보 | ❌ FAIL | 0/1 | API 응닡 404 | +| 3 | νšŒκ³„κ΄€λ¦¬ > κ³„μ’Œμž…μΆœκΈˆλ‚΄μ—­ | ❌ FAIL | 0/1 | λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) | +| 4 | νšŒκ³„κ΄€λ¦¬ > μΉ΄λ“œμ‚¬μš©λ‚΄μ—­ | ❌ FAIL | 0/1 | λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) | +| 5 | νšŒκ³„κ΄€λ¦¬ > μ§€μΆœμ˜ˆμƒλ‚΄μ—­μ„œ | ⏭️ NO_BUTTON | - | λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€, PDF) | +| 6 | νšŒκ³„κ΄€λ¦¬ > μ„ΈκΈˆκ³„μ‚°μ„œκ΄€λ¦¬ | ❌ FAIL | 0/1 | λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) | +| 7 | νšŒκ³„κ΄€λ¦¬ > κ²°μ œλ‚΄μ—­ | ⏭️ NO_BUTTON | - | λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€, Excel, λ‹€μš΄λ‘œλ“œ) | +| 8 | νšŒκ³„κ΄€λ¦¬ > λ§€μΆœκ΄€λ¦¬ | ⏭️ NO_BUTTON | - | λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€) | +| 9 | νšŒκ³„κ΄€λ¦¬ > λ―Έμˆ˜κΈˆν˜„ν™© | ❌ FAIL | 0/1 | λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) | +| 10 | νšŒκ³„κ΄€λ¦¬ > μΆœκΈˆκ΄€λ¦¬ | ⏭️ NO_BUTTON | - | λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€, λ‹€μš΄λ‘œλ“œ) | +| 11 | νšŒκ³„κ΄€λ¦¬ > κ±°λž˜μ²˜μ›μž₯ | ❌ FAIL | 0/1 | λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) | +| 12 | κ²°μž¬κ΄€λ¦¬ > κ²°μž¬ν•¨ | ❌ FAIL | 0/1 | API 응닡 500 | +| 13 | κ²°μž¬κ΄€λ¦¬ > 참쑰함 | ❌ FAIL | 0/1 | API 응닡 500 | +| 14 | 인사관리 > κ·Όνƒœν˜„ν™© | ⏭️ NO_BUTTON | - | λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€) | +| 15 | μžμž¬κ΄€λ¦¬ > μž¬κ³ ν˜„ν™© | ❌ FAIL | 0/1 | λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) | +| 16 | ꡬ맀관리 > κ΅¬λ§€ν˜„ν™© | ⏭️ NO_BUTTON | - | λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€, Excel) | +| 17 | 생산관리 > μž‘μ—…μ‹€μ  | βœ… PASS | 1/1 | | +| 18 | νŒλ§€κ΄€λ¦¬ > 단가관리 | ⏭️ NO_BUTTON | - | λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€) | +| 19 | νŒλ§€κ΄€λ¦¬ > κ±°λž˜μ²˜κ΄€λ¦¬ | ⏭️ NO_BUTTON | - | λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€) | +| 20 | νŒλ§€κ΄€λ¦¬ > 견적관리 | ⏭️ NO_BUTTON | - | λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: PDF, 좜λ ₯) | + +## πŸ“‚ λ‹€μš΄λ‘œλ“œ 상세 κ²°κ³Ό + +### νšŒκ³„κ΄€λ¦¬ > 일일 일보 + +| λ²„νŠΌ | μœ ν˜• | κ²°κ³Ό | 파일λͺ… | 크기 | λΉ„κ³  | +|------|------|------|--------|------|------| +| μ—‘μ…€ | excel | ❌ | - | - | API 응닡 404 | + +### νšŒκ³„κ΄€λ¦¬ > κ³„μ’Œμž…μΆœκΈˆλ‚΄μ—­ + +| λ²„νŠΌ | μœ ν˜• | κ²°κ³Ό | 파일λͺ… | 크기 | λΉ„κ³  | +|------|------|------|--------|------|------| +| μ—‘μ…€ λ‹€μš΄λ‘œλ“œ | excel | ❌ | - | - | λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) | + +### νšŒκ³„κ΄€λ¦¬ > μΉ΄λ“œμ‚¬μš©λ‚΄μ—­ + +| λ²„νŠΌ | μœ ν˜• | κ²°κ³Ό | 파일λͺ… | 크기 | λΉ„κ³  | +|------|------|------|--------|------|------| +| μ—‘μ…€ λ‹€μš΄λ‘œλ“œ | excel | ❌ | - | - | λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) | + +### νšŒκ³„κ΄€λ¦¬ > μ„ΈκΈˆκ³„μ‚°μ„œκ΄€λ¦¬ + +| λ²„νŠΌ | μœ ν˜• | κ²°κ³Ό | 파일λͺ… | 크기 | λΉ„κ³  | +|------|------|------|--------|------|------| +| μ—‘μ…€ λ‹€μš΄λ‘œλ“œ | excel | ❌ | - | - | λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) | + +### νšŒκ³„κ΄€λ¦¬ > λ―Έμˆ˜κΈˆν˜„ν™© + +| λ²„νŠΌ | μœ ν˜• | κ²°κ³Ό | 파일λͺ… | 크기 | λΉ„κ³  | +|------|------|------|--------|------|------| +| μ—‘μ…€ λ‹€μš΄λ‘œλ“œ | excel | ❌ | - | - | λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) | + +### νšŒκ³„κ΄€λ¦¬ > κ±°λž˜μ²˜μ›μž₯ + +| λ²„νŠΌ | μœ ν˜• | κ²°κ³Ό | 파일λͺ… | 크기 | λΉ„κ³  | +|------|------|------|--------|------|------| +| μ—‘μ…€ λ‹€μš΄λ‘œλ“œ | excel | ❌ | - | - | λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) | + +### κ²°μž¬κ΄€λ¦¬ > κ²°μž¬ν•¨ + +| λ²„νŠΌ | μœ ν˜• | κ²°κ³Ό | 파일λͺ… | 크기 | λΉ„κ³  | +|------|------|------|--------|------|------| +| PDF | pdf | ❌ | - | - | API 응닡 500 | + +### κ²°μž¬κ΄€λ¦¬ > 참쑰함 + +| λ²„νŠΌ | μœ ν˜• | κ²°κ³Ό | 파일λͺ… | 크기 | λΉ„κ³  | +|------|------|------|--------|------|------| +| PDF | pdf | ❌ | - | - | API 응닡 500 | + +### μžμž¬κ΄€λ¦¬ > μž¬κ³ ν˜„ν™© + +| λ²„νŠΌ | μœ ν˜• | κ²°κ³Ό | 파일λͺ… | 크기 | λΉ„κ³  | +|------|------|------|--------|------|------| +| μ—‘μ…€ λ‹€μš΄λ‘œλ“œ | excel | ❌ | - | - | λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) | + +### 생산관리 > μž‘μ—…μ‹€μ  + +| λ²„νŠΌ | μœ ν˜• | κ²°κ³Ό | 파일λͺ… | 크기 | λΉ„κ³  | +|------|------|------|--------|------|------| +| μ—‘μ…€ λ‹€μš΄λ‘œλ“œ | excel | βœ… | μž‘μ—…μ‹€μ _20260304_201548.xlsx | 17.2KB | | + +## πŸ“ λ‹€μš΄λ‘œλ“œλœ 파일 λͺ©λ‘ + +| # | 파일λͺ… | 크기 | +|---|--------|------| +| 1 | μž‘μ—…μ‹€μ _20260304_201548.xlsx | 17.2KB | + +## ❌ μ‹€νŒ¨/이슈 ν•­λͺ© + +- **νšŒκ³„κ΄€λ¦¬ > λ§€μž…κ΄€λ¦¬**: NO_BUTTON - λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€) +- **νšŒκ³„κ΄€λ¦¬ > 일일 일보**: FAIL - μ—‘μ…€: API 응닡 404 +- **νšŒκ³„κ΄€λ¦¬ > κ³„μ’Œμž…μΆœκΈˆλ‚΄μ—­**: FAIL - μ—‘μ…€ λ‹€μš΄λ‘œλ“œ: λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) +- **νšŒκ³„κ΄€λ¦¬ > μΉ΄λ“œμ‚¬μš©λ‚΄μ—­**: FAIL - μ—‘μ…€ λ‹€μš΄λ‘œλ“œ: λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) +- **νšŒκ³„κ΄€λ¦¬ > μ§€μΆœμ˜ˆμƒλ‚΄μ—­μ„œ**: NO_BUTTON - λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€, PDF) +- **νšŒκ³„κ΄€λ¦¬ > μ„ΈκΈˆκ³„μ‚°μ„œκ΄€λ¦¬**: FAIL - μ—‘μ…€ λ‹€μš΄λ‘œλ“œ: λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) +- **νšŒκ³„κ΄€λ¦¬ > κ²°μ œλ‚΄μ—­**: NO_BUTTON - λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€, Excel, λ‹€μš΄λ‘œλ“œ) +- **νšŒκ³„κ΄€λ¦¬ > λ§€μΆœκ΄€λ¦¬**: NO_BUTTON - λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€) +- **νšŒκ³„κ΄€λ¦¬ > λ―Έμˆ˜κΈˆν˜„ν™©**: FAIL - μ—‘μ…€ λ‹€μš΄λ‘œλ“œ: λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) +- **νšŒκ³„κ΄€λ¦¬ > μΆœκΈˆκ΄€λ¦¬**: NO_BUTTON - λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€, λ‹€μš΄λ‘œλ“œ) +- **νšŒκ³„κ΄€λ¦¬ > κ±°λž˜μ²˜μ›μž₯**: FAIL - μ—‘μ…€ λ‹€μš΄λ‘œλ“œ: λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) +- **κ²°μž¬κ΄€λ¦¬ > κ²°μž¬ν•¨**: FAIL - PDF: API 응닡 500 +- **κ²°μž¬κ΄€λ¦¬ > 참쑰함**: FAIL - PDF: API 응닡 500 +- **인사관리 > κ·Όνƒœν˜„ν™©**: NO_BUTTON - λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€) +- **μžμž¬κ΄€λ¦¬ > μž¬κ³ ν˜„ν™©**: FAIL - μ—‘μ…€ λ‹€μš΄λ‘œλ“œ: λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ) +- **ꡬ맀관리 > κ΅¬λ§€ν˜„ν™©**: NO_BUTTON - λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€, Excel) +- **νŒλ§€κ΄€λ¦¬ > 단가관리**: NO_BUTTON - λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€) +- **νŒλ§€κ΄€λ¦¬ > κ±°λž˜μ²˜κ΄€λ¦¬**: NO_BUTTON - λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: μ—‘μ…€) +- **νŒλ§€κ΄€λ¦¬ > 견적관리**: NO_BUTTON - λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: PDF, 좜λ ₯) diff --git a/e2e/results/hotfix/downloads/prod-work-result_μž‘μ—…μ‹€μ _20260304_201548.xlsx b/e2e/results/hotfix/downloads/prod-work-result_μž‘μ—…μ‹€μ _20260304_201548.xlsx new file mode 100644 index 0000000..02eddcb Binary files /dev/null and b/e2e/results/hotfix/downloads/prod-work-result_μž‘μ—…μ‹€μ _20260304_201548.xlsx differ diff --git a/e2e/runner/download-debug.js b/e2e/runner/download-debug.js new file mode 100644 index 0000000..75e7aeb --- /dev/null +++ b/e2e/runner/download-debug.js @@ -0,0 +1,424 @@ +#!/usr/bin/env node +/** + * λ‹€μš΄λ‘œλ“œ λ²„νŠΌ 상세 디버깅 슀크립트 + * + * 각 νŽ˜μ΄μ§€μ˜ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ 클릭 μ‹œ μ‹€μ œλ‘œ 무엇이 μΌμ–΄λ‚˜λŠ”μ§€ 좔적 + * - λͺ¨λ“  λ„€νŠΈμ›Œν¬ μš”μ²­ λ‘œκΉ… + * - Blob URL 생성 감지 + * - μƒˆ νƒ­/νŒμ—… 감지 + * - Console λ©”μ‹œμ§€ 캑처 + */ + +const fs = require('fs'); +const path = require('path'); + +const SAM_ROOT = path.resolve(__dirname, '..', '..'); +const PW_PATH = path.join(SAM_ROOT, 'react', 'node_modules', 'playwright'); +const { chromium } = require(PW_PATH); + +const BASE_URL = 'https://dev.codebridge-x.com'; +const AUTH = { username: 'TestUser5', password: 'password123!' }; +const RESULTS_DIR = path.join(SAM_ROOT, 'e2e', 'results', 'hotfix'); +const DOWNLOAD_DIR = path.join(RESULTS_DIR, 'downloads'); + +const C = { + green: t => `\x1b[32m${t}\x1b[0m`, + red: t => `\x1b[31m${t}\x1b[0m`, + yellow: t => `\x1b[33m${t}\x1b[0m`, + cyan: t => `\x1b[36m${t}\x1b[0m`, + dim: t => `\x1b[2m${t}\x1b[0m`, + bold: t => `\x1b[1m${t}\x1b[0m`, +}; + +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } + +function getTimestamp() { + const n = new Date(); + const pad = v => v.toString().padStart(2, '0'); + return `${n.getFullYear()}-${pad(n.getMonth() + 1)}-${pad(n.getDate())}_${pad(n.getHours())}-${pad(n.getMinutes())}-${pad(n.getSeconds())}`; +} + +// μ‹€νŒ¨ν•œ νŽ˜μ΄μ§€λ§Œ μž¬κ²€μ¦ (λ²„νŠΌμ€ μžˆλŠ”λ° λ‹€μš΄λ‘œλ“œ μ‹€νŒ¨ν•œ 것) +const DEBUG_TARGETS = [ + { id: 'acc-daily-report', level1: 'νšŒκ³„κ΄€λ¦¬', level2: '일일 일보' }, + { id: 'acc-bank-tx', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'κ³„μ’Œμž…μΆœκΈˆλ‚΄μ—­' }, + { id: 'acc-card-history', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'μΉ΄λ“œμ‚¬μš©λ‚΄μ—­' }, + { id: 'acc-tax', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'μ„ΈκΈˆκ³„μ‚°μ„œκ΄€λ¦¬' }, + { id: 'acc-receivable', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'λ―Έμˆ˜κΈˆν˜„ν™©' }, + { id: 'acc-vendor-ledger', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'κ±°λž˜μ²˜μ›μž₯' }, + { id: 'material-stock', level1: 'μžμž¬κ΄€λ¦¬', level2: 'μž¬κ³ ν˜„ν™©' }, + // NO_BUTTON νŽ˜μ΄μ§€λ„ μž¬ν™•μΈ - μ‹€μ œ μ–΄λ–€ λ²„νŠΌλ“€μ΄ μžˆλŠ”μ§€ + { id: 'acc-purchase', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'λ§€μž…κ΄€λ¦¬', scanOnly: true }, + { id: 'acc-expense', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'μ§€μΆœμ˜ˆμƒλ‚΄μ—­μ„œ', scanOnly: true }, + { id: 'acc-payment', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'κ²°μ œλ‚΄μ—­', scanOnly: true }, + { id: 'acc-sales', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'λ§€μΆœκ΄€λ¦¬', scanOnly: true }, + { id: 'acc-withdrawal', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'μΆœκΈˆκ΄€λ¦¬', scanOnly: true }, + { id: 'hr-attendance', level1: '인사관리', level2: 'κ·Όνƒœν˜„ν™©', scanOnly: true }, + { id: 'purchase-status', level1: 'ꡬ맀관리', level2: 'κ΅¬λ§€ν˜„ν™©', scanOnly: true }, + { id: 'sales-pricing', level1: 'νŒλ§€κ΄€λ¦¬', level2: '단가관리', scanOnly: true }, + { id: 'sales-client', level1: 'νŒλ§€κ΄€λ¦¬', level2: 'κ±°λž˜μ²˜κ΄€λ¦¬', scanOnly: true }, + { id: 'sales-quotation', level1: 'νŒλ§€κ΄€λ¦¬', level2: '견적관리', scanOnly: true }, +]; + +async function main() { + [DOWNLOAD_DIR].forEach(d => { + if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); + }); + + console.log(C.bold('\n╔══════════════════════════════════════════════════╗')); + console.log(C.bold('β•‘ πŸ” λ‹€μš΄λ‘œλ“œ λ²„νŠΌ 상세 디버깅 β•‘')); + console.log(C.bold('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\n')); + + const browser = await chromium.launch({ + headless: false, + args: ['--window-position=1920,0', '--window-size=1920,1080'], + }); + + const context = await browser.newContext({ + viewport: { width: 1920, height: 1080 }, + locale: 'ko-KR', + acceptDownloads: true, + }); + + const page = await context.newPage(); + + // 둜그인 + console.log(C.cyan('πŸ” 둜그인...')); + await page.goto(`${BASE_URL}/login`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.fill('input[type="text"], input[name="username"], #username', AUTH.username); + await page.fill('input[type="password"], input[name="password"], #password', AUTH.password); + await page.click('button[type="submit"]'); + await page.waitForURL('**/dashboard**', { timeout: 15000 }); + console.log(C.green('βœ… 둜그인 성곡\n')); + + const allResults = []; + + for (let i = 0; i < DEBUG_TARGETS.length; i++) { + const target = DEBUG_TARGETS[i]; + console.log(C.bold(`\n[${ i + 1}/${DEBUG_TARGETS.length}] ${target.level1} > ${target.level2} ${target.scanOnly ? '(λ²„νŠΌ μŠ€μΊ”λ§Œ)' : '(λ‹€μš΄λ‘œλ“œ 디버깅)'}`)); + console.log(C.dim('─'.repeat(60))); + + // λŒ€μ‹œλ³΄λ“œλ‘œ 이동 + await page.goto(`${BASE_URL}/dashboard`, { waitUntil: 'domcontentloaded', timeout: 15000 }); + await sleep(1000); + + // 메뉴 탐색 + const navOk = await navigateViaMenu(page, target.level1, target.level2); + if (!navOk) { + console.log(C.red(' ❌ 메뉴 탐색 μ‹€νŒ¨')); + allResults.push({ ...target, status: 'NAV_FAIL' }); + continue; + } + await sleep(2500); + + const currentUrl = page.url(); + console.log(C.dim(` πŸ“ ${currentUrl}`)); + + if (target.scanOnly) { + // λ²„νŠΌ μŠ€μΊ”λ§Œ + const buttons = await page.evaluate(() => { + const allBtns = Array.from(document.querySelectorAll('button, a[role="button"], [role="button"]')); + return allBtns + .filter(b => { + const rect = b.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }) + .map(b => ({ + text: (b.innerText?.trim() || '').substring(0, 50), + ariaLabel: b.getAttribute('aria-label') || '', + className: (b.className || '').substring(0, 60), + tagName: b.tagName, + })) + .filter(b => b.text || b.ariaLabel); + }); + + // λ‹€μš΄λ‘œλ“œ κ΄€λ ¨ λ²„νŠΌ ν•„ν„° + const dlKeywords = ['μ—‘μ…€', 'Excel', 'excel', 'λ‹€μš΄λ‘œλ“œ', 'download', 'Download', 'PDF', 'pdf', 'Export', 'export', '내보내기', '좜λ ₯', '인쇄', 'CSV', 'csv']; + const dlButtons = buttons.filter(b => { + const combined = `${b.text} ${b.ariaLabel}`; + return dlKeywords.some(kw => combined.includes(kw)); + }); + + if (dlButtons.length > 0) { + console.log(C.yellow(` πŸ“‹ λ‹€μš΄λ‘œλ“œ κ΄€λ ¨ λ²„νŠΌ 발견 ${dlButtons.length}개:`)); + dlButtons.forEach(b => console.log(C.yellow(` "${b.text}" (aria: ${b.ariaLabel || 'none'})`))); + } else { + console.log(C.dim(` ℹ️ λ‹€μš΄λ‘œλ“œ κ΄€λ ¨ λ²„νŠΌ μ—†μŒ`)); + // 전체 λ²„νŠΌ λͺ©λ‘ 좜λ ₯ (λ””λ²„κΉ…μš©) + console.log(C.dim(` 전체 λ²„νŠΌ ${buttons.length}개: ${buttons.slice(0, 10).map(b => `"${b.text}"`).join(', ')}${buttons.length > 10 ? '...' : ''}`)); + } + + allResults.push({ ...target, status: dlButtons.length > 0 ? 'HAS_BUTTON' : 'NO_BUTTON', buttons: dlButtons, allButtons: buttons.slice(0, 15) }); + continue; + } + + // ───── λ‹€μš΄λ‘œλ“œ 디버깅 (FAIL νŽ˜μ΄μ§€) ───── + // 1. λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ°ΎκΈ° + const dlButtons = await page.evaluate(() => { + const kw = ['μ—‘μ…€', 'Excel', 'excel', 'λ‹€μš΄λ‘œλ“œ', 'download', 'PDF', 'pdf', 'Export', 'export', '내보내기', '좜λ ₯', 'CSV']; + const allBtns = Array.from(document.querySelectorAll('button, a[role="button"], [role="button"], a')); + const found = []; + for (const btn of allBtns) { + const text = btn.innerText?.trim() || ''; + const ariaLabel = btn.getAttribute('aria-label') || ''; + const href = btn.getAttribute('href') || ''; + const combined = `${text} ${ariaLabel} ${href}`; + const rect = btn.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) continue; + if (kw.some(k => combined.includes(k))) { + found.push({ + text: text.substring(0, 50), + ariaLabel, + href, + tagName: btn.tagName, + disabled: btn.disabled || btn.getAttribute('aria-disabled') === 'true', + index: allBtns.indexOf(btn), + }); + } + } + return found; + }); + + if (dlButtons.length === 0) { + console.log(C.yellow(' ⚠️ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ')); + allResults.push({ ...target, status: 'NO_BUTTON' }); + continue; + } + + for (const btn of dlButtons) { + console.log(C.cyan(` πŸ”˜ λ²„νŠΌ: "${btn.text}" (tag: ${btn.tagName}, disabled: ${btn.disabled})`)); + + if (btn.disabled) { + console.log(C.yellow(` ⚠️ λ²„νŠΌ λΉ„ν™œμ„±ν™” μƒνƒœ`)); + allResults.push({ ...target, status: 'DISABLED', button: btn.text }); + continue; + } + + // 2. λ„€νŠΈμ›Œν¬ μš”μ²­ κ°μ‹œ μ‹œμž‘ + const networkLogs = []; + const reqHandler = req => { + const url = req.url(); + if (!url.includes('_next/static') && !url.includes('favicon') && !url.includes('chunk')) { + networkLogs.push({ type: 'request', method: req.method(), url: url.substring(0, 120) }); + } + }; + const resHandler = res => { + const url = res.url(); + if (!url.includes('_next/static') && !url.includes('favicon') && !url.includes('chunk')) { + networkLogs.push({ + type: 'response', status: res.status(), url: url.substring(0, 120), + contentType: res.headers()['content-type']?.substring(0, 50) || '', + }); + } + }; + + page.on('request', reqHandler); + page.on('response', resHandler); + + // Console 둜그 κ°μ‹œ + const consoleLogs = []; + const consoleHandler = msg => consoleLogs.push(`[${msg.type()}] ${msg.text().substring(0, 100)}`); + page.on('console', consoleHandler); + + // λ‹€μš΄λ‘œλ“œ 이벀트 κ°μ‹œ + let downloadEvent = null; + const downloadHandler = dl => { downloadEvent = dl; }; + page.on('download', downloadHandler); + + // νŒμ—… κ°μ‹œ + let popupPage = null; + const popupHandler = p => { popupPage = p; }; + context.on('page', popupHandler); + + // 3. λ²„νŠΌ 클릭 + await page.evaluate((idx) => { + const allBtns = Array.from(document.querySelectorAll('button, a[role="button"], [role="button"], a')); + if (allBtns[idx]) allBtns[idx].click(); + }, btn.index); + + // 4. 8초 λŒ€κΈ° + await sleep(8000); + + // 5. λ‹€μš΄λ‘œλ“œ 처리 + if (downloadEvent) { + const fname = downloadEvent.suggestedFilename(); + const savePath = path.join(DOWNLOAD_DIR, `${target.id}_${fname}`); + await downloadEvent.saveAs(savePath); + const stat = fs.statSync(savePath); + console.log(C.green(` βœ… λ‹€μš΄λ‘œλ“œ 성곡: ${fname} (${(stat.size / 1024).toFixed(1)}KB)`)); + allResults.push({ ...target, status: 'PASS', button: btn.text, file: fname, size: stat.size }); + } else if (popupPage) { + console.log(C.yellow(` πŸ“‹ μƒˆ νƒ­/νŒμ—… μ—΄λ¦Ό: ${popupPage.url().substring(0, 80)}`)); + try { await popupPage.close(); } catch {} + allResults.push({ ...target, status: 'POPUP', button: btn.text, popupUrl: popupPage.url() }); + } else { + // λ„€νŠΈμ›Œν¬ 둜그 뢄석 + const apiCalls = networkLogs.filter(l => l.type === 'response' && !l.url.includes('_next')); + console.log(C.dim(` λ„€νŠΈμ›Œν¬ 응닡 ${apiCalls.length}건:`)); + apiCalls.forEach(l => { + const icon = l.status >= 200 && l.status < 300 ? '🟒' : l.status >= 400 ? 'πŸ”΄' : '🟑'; + console.log(C.dim(` ${icon} ${l.status} ${l.url} [${l.contentType}]`)); + }); + + // Blob URL λ˜λŠ” ν† μŠ€νŠΈ 확인 + const blobCheck = await page.evaluate(() => { + const toast = document.querySelector('[class*="toast"], [class*="Toastify"], [role="alert"]'); + const anchors = Array.from(document.querySelectorAll('a')).filter(a => a.href?.startsWith('blob:')); + return { + toast: toast?.innerText?.trim() || null, + blobUrls: anchors.map(a => a.href), + }; + }); + + if (blobCheck.toast) console.log(C.yellow(` πŸ“Œ ν† μŠ€νŠΈ: "${blobCheck.toast}"`)); + if (blobCheck.blobUrls.length > 0) console.log(C.cyan(` πŸ“Ž Blob URL 발견: ${blobCheck.blobUrls.length}개`)); + + if (consoleLogs.length > 0) { + const interesting = consoleLogs.filter(l => !l.includes('[info]') || l.includes('download') || l.includes('export') || l.includes('error')); + if (interesting.length > 0) { + console.log(C.dim(` Console: ${interesting.slice(0, 3).join('; ')}`)); + } + } + + // μ„œλ²„ μ•‘μ…˜ POST 확인 + const serverActions = networkLogs.filter(l => l.type === 'request' && l.method === 'POST'); + if (serverActions.length > 0) { + console.log(C.yellow(` πŸ“€ POST μš”μ²­ ${serverActions.length}건:`)); + serverActions.forEach(l => console.log(C.dim(` ${l.url}`))); + } + + allResults.push({ ...target, status: 'NO_DOWNLOAD', button: btn.text, network: apiCalls, serverActions }); + } + + // λ¦¬μŠ€λ„ˆ ν•΄μ œ + page.off('request', reqHandler); + page.off('response', resHandler); + page.off('console', consoleHandler); + page.off('download', downloadHandler); + context.off('page', popupHandler); + + // λͺ¨λ‹¬ λ‹«κΈ° + await page.evaluate(async () => { + for (let i = 0; i < 3; i++) { + const modal = document.querySelector("[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip'])"); + if (!modal || modal.getBoundingClientRect().width === 0) break; + const closeBtn = modal.querySelector("button[class*='close'], [aria-label='λ‹«κΈ°']") + || Array.from(modal.querySelectorAll('button')).find(b => ['λ‹«κΈ°', 'Close', 'μ·¨μ†Œ', '확인'].some(t => b.innerText?.includes(t))); + if (closeBtn) closeBtn.click(); + else document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true })); + await new Promise(r => setTimeout(r, 500)); + } + }); + await sleep(500); + } + } + + // ─── κ²°κ³Ό μš”μ•½ ───────────────────────────────────────────── + console.log(C.bold('\n══════════════════════════════════════════════════')); + console.log(C.bold('πŸ“Š λ‹€μš΄λ‘œλ“œ 디버깅 κ²°κ³Ό μš”μ•½')); + console.log(C.bold('══════════════════════════════════════════════════\n')); + + const pass = allResults.filter(r => r.status === 'PASS'); + const noBtn = allResults.filter(r => r.status === 'NO_BUTTON'); + const hasBtn = allResults.filter(r => r.status === 'HAS_BUTTON'); + const noDl = allResults.filter(r => r.status === 'NO_DOWNLOAD'); + const disabled = allResults.filter(r => r.status === 'DISABLED'); + + console.log(` ${C.green(`βœ… λ‹€μš΄λ‘œλ“œ 성곡: ${pass.length}개`)} ${pass.map(r => r.id).join(', ')}`); + console.log(` ${C.red(`❌ λ²„νŠΌ μžˆμœΌλ‚˜ λ‹€μš΄λ‘œλ“œ μ•ˆλ¨: ${noDl.length}개`)} ${noDl.map(r => r.id).join(', ')}`); + console.log(` ${C.yellow(`⚠️ λ²„νŠΌ λΉ„ν™œμ„±ν™”: ${disabled.length}개`)}`); + console.log(` ${C.yellow(`πŸ”˜ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ 발견(μŠ€μΊ”): ${hasBtn.length}개`)} ${hasBtn.map(r => `${r.id}(${r.buttons.map(b=>b.text).join(',')})`).join(', ')}`); + console.log(` ${C.dim(`⏭️ λ²„νŠΌ μ—†μŒ: ${noBtn.length}개`)} ${noBtn.map(r => r.id).join(', ')}`); + + // 리포트 μ €μž₯ + const ts = getTimestamp(); + const reportLines = ['# πŸ” λ‹€μš΄λ‘œλ“œ 디버깅 상세 리포트\n', `**μ‹€ν–‰ μ‹œκ°„**: ${ts}\n`]; + + reportLines.push('## μΉ΄ν…Œκ³ λ¦¬λ³„ κ²°κ³Ό\n'); + + if (pass.length > 0) { + reportLines.push('### βœ… λ‹€μš΄λ‘œλ“œ 성곡\n'); + pass.forEach(r => reportLines.push(`- **${r.level1} > ${r.level2}**: ${r.button} β†’ ${r.file} (${(r.size/1024).toFixed(1)}KB)\n`)); + } + + if (noDl.length > 0) { + reportLines.push('\n### ❌ λ²„νŠΌ μžˆμœΌλ‚˜ λ‹€μš΄λ‘œλ“œ λ―Έλ™μž‘\n'); + noDl.forEach(r => { + reportLines.push(`- **${r.level1} > ${r.level2}**: "${r.button}"\n`); + if (r.network?.length > 0) { + reportLines.push(` - λ„€νŠΈμ›Œν¬ 응닡: ${r.network.map(n => `${n.status} ${n.url}`).join('; ')}\n`); + } + if (r.serverActions?.length > 0) { + reportLines.push(` - Server Actions: ${r.serverActions.map(s => s.url).join('; ')}\n`); + } + }); + } + + if (hasBtn.length > 0) { + reportLines.push('\n### πŸ”˜ λ²„νŠΌ 발견 (μŠ€μΊ” κ²°κ³Ό)\n'); + hasBtn.forEach(r => { + reportLines.push(`- **${r.level1} > ${r.level2}**: ${r.buttons.map(b => `"${b.text}"`).join(', ')}\n`); + }); + } + + if (noBtn.length > 0) { + reportLines.push('\n### ⏭️ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ\n'); + noBtn.forEach(r => { + const sample = r.allButtons?.slice(0, 8).map(b => `"${b.text}"`).join(', ') || 'N/A'; + reportLines.push(`- **${r.level1} > ${r.level2}**: μ£Όμš” λ²„νŠΌ: ${sample}\n`); + }); + } + + const reportPath = path.join(RESULTS_DIR, `Download-Debug_${ts}.md`); + fs.writeFileSync(reportPath, reportLines.join(''), 'utf-8'); + console.log(C.cyan(`\nπŸ“„ 리포트: ${reportPath}`)); + + await browser.close(); + console.log(C.dim('πŸ”’ λΈŒλΌμš°μ € λ‹«νž˜\n')); +} + +// ─── μ‚¬μ΄λ“œλ°” 메뉴 탐색 ───────────────────────────────────── +async function navigateViaMenu(page, level1, level2) { + try { + await page.evaluate(() => { + const sidebar = document.querySelector('.sidebar-scroll, [class*="sidebar"], nav'); + if (sidebar) sidebar.scrollTo({ top: 0, behavior: 'instant' }); + }); + await sleep(300); + + for (let scroll = 0; scroll < 15; scroll++) { + const found = await page.evaluate((l1) => { + const btns = Array.from(document.querySelectorAll('button, [role="button"], a')); + const btn = btns.find(b => b.innerText?.trim().startsWith(l1)); + if (btn) { btn.click(); return true; } + return false; + }, level1); + + if (found) { + await sleep(500); + const nav2 = await page.evaluate((l2) => { + const items = Array.from(document.querySelectorAll('a, button')); + const item = items.find(el => el.innerText?.trim() === l2); + if (item) { item.click(); return true; } + return false; + }, level2); + if (nav2) { + await sleep(2000); + return true; + } + } + + await page.evaluate(() => { + const sidebar = document.querySelector('.sidebar-scroll, [class*="sidebar"], nav'); + if (sidebar) sidebar.scrollBy({ top: 150, behavior: 'instant' }); + }); + await sleep(100); + } + return false; + } catch { return false; } +} + +main().catch(err => { + console.error(C.red(`πŸ’₯ 였λ₯˜: ${err.message}`)); + process.exit(1); +}); diff --git a/e2e/runner/download-verify.js b/e2e/runner/download-verify.js new file mode 100644 index 0000000..5245a0d --- /dev/null +++ b/e2e/runner/download-verify.js @@ -0,0 +1,553 @@ +#!/usr/bin/env node +/** + * λ‹€μš΄λ‘œλ“œ κΈ°λŠ₯ μ „μš© 검증 슀크립트 + * + * μ—‘μ…€/PDF λ‹€μš΄λ‘œλ“œ λ²„νŠΌμ΄ μžˆλŠ” λͺ¨λ“  νŽ˜μ΄μ§€λ₯Ό μˆœνšŒν•˜λ©° + * μ‹€μ œ λ‹€μš΄λ‘œλ“œκ°€ λ™μž‘ν•˜λŠ”μ§€ κ²€μ¦ν•©λ‹ˆλ‹€. + * + * Usage: + * node e2e/runner/download-verify.js # 전체 μ‹€ν–‰ + * node e2e/runner/download-verify.js --headless # λ°±κ·ΈλΌμš΄λ“œ μ‹€ν–‰ + */ + +const fs = require('fs'); +const path = require('path'); + +const SAM_ROOT = path.resolve(__dirname, '..', '..'); +const PW_PATH = path.join(SAM_ROOT, 'react', 'node_modules', 'playwright'); +const { chromium } = require(PW_PATH); + +const BASE_URL = 'https://dev.codebridge-x.com'; +const AUTH = { username: 'TestUser5', password: 'password123!' }; +const RESULTS_DIR = path.join(SAM_ROOT, 'e2e', 'results', 'hotfix'); +const DOWNLOAD_DIR = path.join(RESULTS_DIR, 'downloads'); + +const args = process.argv.slice(2); +const HEADLESS = args.includes('--headless'); + +// ─── λ‹€μš΄λ‘œλ“œ ν…ŒμŠ€νŠΈ λŒ€μƒ νŽ˜μ΄μ§€ μ •μ˜ ───────────────────────── +const DOWNLOAD_TARGETS = [ + // νšŒκ³„κ΄€λ¦¬ + { id: 'acc-purchase', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'λ§€μž…κ΄€λ¦¬', url: '/accounting/purchase-accounting', buttons: ['μ—‘μ…€'] }, + { id: 'acc-daily-report', level1: 'νšŒκ³„κ΄€λ¦¬', level2: '일일 일보', url: '/accounting/daily-report', buttons: ['μ—‘μ…€', 'Excel', 'λ‹€μš΄λ‘œλ“œ', 'PDF', '인쇄'] }, + { id: 'acc-bank-tx', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'κ³„μ’Œμž…μΆœκΈˆλ‚΄μ—­', url: '/accounting/bank-transactions', buttons: ['μ—‘μ…€'] }, + { id: 'acc-card-history', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'μΉ΄λ“œμ‚¬μš©λ‚΄μ—­', url: '/accounting/card-transactions', buttons: ['μ—‘μ…€'] }, + { id: 'acc-expense', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'μ§€μΆœμ˜ˆμƒλ‚΄μ—­μ„œ', url: '/accounting/expected-expenses', buttons: ['μ—‘μ…€', 'PDF'] }, + { id: 'acc-tax', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'μ„ΈκΈˆκ³„μ‚°μ„œκ΄€λ¦¬', url: '/accounting/tax-invoice', buttons: ['μ—‘μ…€', 'Excel'] }, + { id: 'acc-payment', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'κ²°μ œλ‚΄μ—­', url: '/payment-history', buttons: ['μ—‘μ…€', 'Excel', 'λ‹€μš΄λ‘œλ“œ'] }, + { id: 'acc-sales', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'λ§€μΆœκ΄€λ¦¬', url: '/accounting/sales-accounting', buttons: ['μ—‘μ…€'] }, + { id: 'acc-receivable', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'λ―Έμˆ˜κΈˆν˜„ν™©', url: '/accounting/receivables-status', buttons: ['μ—‘μ…€', 'λ‹€μš΄λ‘œλ“œ', '내보내기'] }, + { id: 'acc-withdrawal', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'μΆœκΈˆκ΄€λ¦¬', url: '/ko/accounting/withdrawals', buttons: ['μ—‘μ…€', 'λ‹€μš΄λ‘œλ“œ'] }, + { id: 'acc-vendor-ledger', level1: 'νšŒκ³„κ΄€λ¦¬', level2: 'κ±°λž˜μ²˜μ›μž₯', url: '/ko/accounting/vendor-ledger', buttons: ['μ—‘μ…€', 'PDF'] }, + // κ²°μž¬κ΄€λ¦¬ + { id: 'approval-box', level1: 'κ²°μž¬κ΄€λ¦¬', level2: 'κ²°μž¬ν•¨', url: '/ko/approval/inbox', buttons: ['PDF'], needsDetail: true }, + { id: 'reference-box', level1: 'κ²°μž¬κ΄€λ¦¬', level2: '참쑰함', url: '/ko/approval/reference', buttons: ['PDF'], needsDetail: true }, + // 인사관리 + { id: 'hr-attendance', level1: '인사관리', level2: 'κ·Όνƒœν˜„ν™©', url: '/hr/attendance', buttons: ['μ—‘μ…€'] }, + // μžμž¬κ΄€λ¦¬ + { id: 'material-stock', level1: 'μžμž¬κ΄€λ¦¬', level2: 'μž¬κ³ ν˜„ν™©', url: '/material/stock-status', buttons: ['μ—‘μ…€', 'λ‹€μš΄λ‘œλ“œ'] }, + // ꡬ맀관리 + { id: 'purchase-status', level1: 'ꡬ맀관리', level2: 'κ΅¬λ§€ν˜„ν™©', url: '/purchase/status', buttons: ['μ—‘μ…€', 'Excel'] }, + // 생산관리 + { id: 'prod-work-result', level1: '생산관리', level2: 'μž‘μ—…μ‹€μ ', url: '/production/work-results', buttons: ['μ—‘μ…€', 'Excel', 'λ‹€μš΄λ‘œλ“œ'] }, + // νŒλ§€κ΄€λ¦¬ + { id: 'sales-pricing', level1: 'νŒλ§€κ΄€λ¦¬', level2: '단가관리', url: '/sales/pricing-management', buttons: ['μ—‘μ…€'] }, + { id: 'sales-client', level1: 'νŒλ§€κ΄€λ¦¬', level2: 'κ±°λž˜μ²˜κ΄€λ¦¬', url: '/sales/client-management-sales-admin', buttons: ['μ—‘μ…€'] }, + { id: 'sales-quotation', level1: 'νŒλ§€κ΄€λ¦¬', level2: '견적관리', url: '/sales/quote-management', buttons: ['PDF', '좜λ ₯'] }, +]; + +// ─── Color helpers ────────────────────────────────────────── +const C = { + green: t => `\x1b[32m${t}\x1b[0m`, + red: t => `\x1b[31m${t}\x1b[0m`, + yellow: t => `\x1b[33m${t}\x1b[0m`, + cyan: t => `\x1b[36m${t}\x1b[0m`, + dim: t => `\x1b[2m${t}\x1b[0m`, + bold: t => `\x1b[1m${t}\x1b[0m`, +}; + +function getTimestamp() { + const n = new Date(); + const pad = v => v.toString().padStart(2, '0'); + return `${n.getFullYear()}-${pad(n.getMonth() + 1)}-${pad(n.getDate())}_${pad(n.getHours())}-${pad(n.getMinutes())}-${pad(n.getSeconds())}`; +} + +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } + +// ─── Main ────────────────────────────────────────────────── +async function main() { + // 디렉토리 μ€€λΉ„ + [RESULTS_DIR, DOWNLOAD_DIR].forEach(d => { + if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); + }); + + console.log(C.bold('\n╔══════════════════════════════════════════════════╗')); + console.log(C.bold('β•‘ πŸ“₯ λ‹€μš΄λ‘œλ“œ κΈ°λŠ₯ 검증 ν…ŒμŠ€νŠΈ β•‘')); + console.log(C.bold('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\n')); + console.log(` λŒ€μƒ: ${DOWNLOAD_TARGETS.length}개 νŽ˜μ΄μ§€`); + console.log(` λͺ¨λ“œ: ${HEADLESS ? 'Headless' : 'Headed'}`); + console.log(` μ €μž₯: ${DOWNLOAD_DIR}\n`); + + const browser = await chromium.launch({ + headless: HEADLESS, + args: HEADLESS ? [] : ['--window-position=1920,0', '--window-size=1920,1080'], + }); + + const context = await browser.newContext({ + viewport: { width: 1920, height: 1080 }, + locale: 'ko-KR', + acceptDownloads: true, + }); + + const page = await context.newPage(); + + // 둜그인 + console.log(C.cyan('πŸ” 둜그인 쀑...')); + await page.goto(`${BASE_URL}/login`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.fill('input[type="text"], input[name="username"], #username', AUTH.username); + await page.fill('input[type="password"], input[name="password"], #password', AUTH.password); + await page.click('button[type="submit"]'); + await page.waitForURL('**/dashboard**', { timeout: 15000 }); + console.log(C.green('βœ… 둜그인 성곡\n')); + + const results = []; + const startTime = Date.now(); + + for (let i = 0; i < DOWNLOAD_TARGETS.length; i++) { + const target = DOWNLOAD_TARGETS[i]; + const progress = `[${i + 1}/${DOWNLOAD_TARGETS.length}]`; + console.log(C.bold(`${progress} ${target.level1} > ${target.level2}`)); + + const pageResult = { + id: target.id, + page: `${target.level1} > ${target.level2}`, + url: target.url, + downloads: [], + status: 'pending', + }; + + try { + // λŒ€μ‹œλ³΄λ“œλ‘œ 이동 + await page.goto(`${BASE_URL}/dashboard`, { waitUntil: 'domcontentloaded', timeout: 15000 }); + await sleep(1000); + + // μ‚¬μ΄λ“œλ°” 메뉴 탐색 + const navSuccess = await navigateViaMenu(page, target.level1, target.level2); + if (!navSuccess) { + pageResult.status = 'NAV_FAIL'; + pageResult.error = 'μ‚¬μ΄λ“œλ°” 메뉴 탐색 μ‹€νŒ¨'; + console.log(C.red(` ❌ 메뉴 탐색 μ‹€νŒ¨: ${target.level1} > ${target.level2}`)); + results.push(pageResult); + continue; + } + + // νŽ˜μ΄μ§€ λ‘œλ“œ λŒ€κΈ° + await sleep(2000); + + // URL 확인 + const currentUrl = page.url(); + console.log(C.dim(` πŸ“ URL: ${currentUrl}`)); + + // κ²°μž¬ν•¨/참쑰함은 상세 νŽ˜μ΄μ§€ μ§„μž… ν•„μš” + if (target.needsDetail) { + const detailOpened = await openFirstDetail(page); + if (!detailOpened) { + pageResult.status = 'NO_DETAIL'; + pageResult.error = '상세 μ§„μž…ν•  데이터 μ—†μŒ'; + console.log(C.yellow(` ⚠️ 상세 μ§„μž…ν•  데이터 μ—†μŒ (PDF λ²„νŠΌμ€ 상세 λͺ¨λ‹¬μ—μ„œλ§Œ κ°€λŠ₯)`)); + results.push(pageResult); + continue; + } + await sleep(1500); + } + + // λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ°ΎκΈ° 및 μ‹€ν–‰ + const foundButtons = await findDownloadButtons(page, target.buttons); + + if (foundButtons.length === 0) { + pageResult.status = 'NO_BUTTON'; + pageResult.error = `λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ—†μŒ (검색: ${target.buttons.join(', ')})`; + console.log(C.yellow(` ⚠️ λ‹€μš΄λ‘œλ“œ λ²„νŠΌμ„ μ°Ύμ§€ λͺ»ν•¨`)); + results.push(pageResult); + continue; + } + + console.log(C.dim(` πŸ“‹ 발견된 λ²„νŠΌ: ${foundButtons.map(b => b.text).join(', ')}`)); + + // 각 λ²„νŠΌ ν΄λ¦­ν•˜μ—¬ λ‹€μš΄λ‘œλ“œ ν…ŒμŠ€νŠΈ + for (const btn of foundButtons) { + const dlResult = await testDownloadButton(page, btn, target); + pageResult.downloads.push(dlResult); + + if (dlResult.success) { + console.log(C.green(` βœ… ${btn.text}: λ‹€μš΄λ‘œλ“œ 성곡 (${dlResult.fileName || dlResult.type})`)); + } else { + console.log(C.red(` ❌ ${btn.text}: ${dlResult.error}`)); + } + + await sleep(1000); + + // λͺ¨λ‹¬μ΄ μ—΄λ €μžˆμœΌλ©΄ λ‹«κΈ° + await closeAllModals(page); + } + + const allSuccess = pageResult.downloads.length > 0 && pageResult.downloads.every(d => d.success); + const someSuccess = pageResult.downloads.some(d => d.success); + pageResult.status = allSuccess ? 'PASS' : someSuccess ? 'PARTIAL' : 'FAIL'; + + } catch (err) { + pageResult.status = 'ERROR'; + pageResult.error = err.message; + console.log(C.red(` ❌ 였λ₯˜: ${err.message}`)); + } + + results.push(pageResult); + console.log(''); + } + + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + + // κ²°κ³Ό μš”μ•½ + const passed = results.filter(r => r.status === 'PASS').length; + const partial = results.filter(r => r.status === 'PARTIAL').length; + const failed = results.filter(r => r.status === 'FAIL').length; + const errors = results.filter(r => ['ERROR', 'NAV_FAIL', 'NO_BUTTON', 'NO_DETAIL'].includes(r.status)).length; + + console.log(C.bold('══════════════════════════════════════════════════')); + console.log(C.bold('πŸ“Š λ‹€μš΄λ‘œλ“œ 검증 κ²°κ³Ό μš”μ•½')); + console.log(C.bold('══════════════════════════════════════════════════')); + console.log(` 전체: ${results.length}개 νŽ˜μ΄μ§€ | μ†Œμš”: ${elapsed}초`); + console.log(` ${C.green(`βœ… PASS: ${passed}`)} | ${C.yellow(`⚠️ PARTIAL: ${partial}`)} | ${C.red(`❌ FAIL: ${failed}`)} | ${C.dim(`⏭️ SKIP/ERROR: ${errors}`)}`); + + // κ°œλ³„ λ‹€μš΄λ‘œλ“œ 톡계 + const allDownloads = results.flatMap(r => r.downloads); + const dlSuccess = allDownloads.filter(d => d.success).length; + const dlFail = allDownloads.filter(d => !d.success).length; + console.log(` λ‹€μš΄λ‘œλ“œ μ‹œλ„: ${allDownloads.length}건 (성곡: ${dlSuccess}, μ‹€νŒ¨: ${dlFail})`); + + // λ‹€μš΄λ‘œλ“œλœ 파일 λͺ©λ‘ + const downloadedFiles = allDownloads.filter(d => d.success && d.fileName); + if (downloadedFiles.length > 0) { + console.log(`\n πŸ“‚ λ‹€μš΄λ‘œλ“œλœ 파일:`); + downloadedFiles.forEach(d => { + console.log(` ${d.fileName} (${d.fileSize ? (d.fileSize / 1024).toFixed(1) + 'KB' : '?'})`); + }); + } + + console.log(''); + + // μ‹€νŒ¨/μ—λŸ¬ λͺ©λ‘ + const failedResults = results.filter(r => !['PASS', 'PARTIAL'].includes(r.status)); + if (failedResults.length > 0) { + console.log(C.red(' ❌ μ‹€νŒ¨/μŠ€ν‚΅ λͺ©λ‘:')); + failedResults.forEach(r => { + console.log(C.red(` ${r.page}: ${r.error || r.status}`)); + }); + console.log(''); + } + + // 리포트 μ €μž₯ + const ts = getTimestamp(); + const reportPath = path.join(RESULTS_DIR, `Download-Verify_${ts}.md`); + const report = generateReport(results, ts, elapsed, downloadedFiles); + fs.writeFileSync(reportPath, report, 'utf-8'); + console.log(C.cyan(`πŸ“„ 리포트 μ €μž₯: ${reportPath}`)); + + await browser.close(); + console.log(C.dim('πŸ”’ λΈŒλΌμš°μ € λ‹«νž˜\n')); +} + +// ─── μ‚¬μ΄λ“œλ°” 메뉴 탐색 ───────────────────────────────────── +async function navigateViaMenu(page, level1, level2) { + try { + // μ‚¬μ΄λ“œλ°” 슀크둀 μ΅œμƒλ‹¨ + await page.evaluate(() => { + const sidebar = document.querySelector('.sidebar-scroll, [class*="sidebar"], nav'); + if (sidebar) sidebar.scrollTo({ top: 0, behavior: 'instant' }); + }); + await sleep(300); + + // level1 메뉴 μ°Ύμ•„μ„œ 클릭 + for (let scroll = 0; scroll < 15; scroll++) { + const found = await page.evaluate((l1) => { + const btns = Array.from(document.querySelectorAll('button, [role="button"], a')); + const btn = btns.find(b => b.innerText?.trim().startsWith(l1)); + if (btn) { btn.click(); return true; } + return false; + }, level1); + + if (found) { + await sleep(500); + + // level2 메뉴 클릭 + const nav2 = await page.evaluate((l2) => { + const items = Array.from(document.querySelectorAll('a, button')); + const item = items.find(el => el.innerText?.trim() === l2); + if (item) { item.click(); return true; } + return false; + }, level2); + + if (nav2) { + await sleep(2000); + return true; + } + } + + // 슀크둀 λ‹€μš΄ + await page.evaluate(() => { + const sidebar = document.querySelector('.sidebar-scroll, [class*="sidebar"], nav'); + if (sidebar) sidebar.scrollBy({ top: 150, behavior: 'instant' }); + }); + await sleep(100); + } + return false; + } catch (e) { + return false; + } +} + +// ─── 첫 번째 데이터 상세 μ—΄κΈ° (κ²°μž¬ν•¨/참쑰함) ───────────────── +async function openFirstDetail(page) { + try { + // ν…Œμ΄λΈ”μ—μ„œ 첫 번째 ν–‰ 클릭 + const clicked = await page.evaluate(() => { + const row = document.querySelector('table tbody tr, [class*="table"] [class*="row"]:not(:first-child)'); + if (row) { row.click(); return true; } + // μΉ΄λ“œ/리슀트 μ•„μ΄ν…œ μ‹œλ„ + const card = document.querySelector('[class*="card"]:not([class*="header"]), [class*="list-item"]'); + if (card) { card.click(); return true; } + return false; + }); + return clicked; + } catch { + return false; + } +} + +// ─── λ‹€μš΄λ‘œλ“œ λ²„νŠΌ μ°ΎκΈ° ────────────────────────────────────── +async function findDownloadButtons(page, keywords) { + return await page.evaluate((kw) => { + const allBtns = Array.from(document.querySelectorAll('button, a[role="button"], [role="button"]')); + const found = []; + const seen = new Set(); + + for (const btn of allBtns) { + const text = btn.innerText?.trim() || ''; + const ariaLabel = btn.getAttribute('aria-label') || ''; + const title = btn.getAttribute('title') || ''; + const combined = `${text} ${ariaLabel} ${title}`; + + if (!text && !ariaLabel && !title) continue; + + for (const keyword of kw) { + if (combined.includes(keyword)) { + const key = text || ariaLabel || title; + if (!seen.has(key)) { + seen.add(key); + // λ²„νŠΌμ΄ λ³΄μ΄λŠ”μ§€ 확인 + const rect = btn.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + found.push({ + text: key, + keyword, + index: allBtns.indexOf(btn), + type: keyword.includes('PDF') || keyword.includes('pdf') ? 'pdf' : 'excel', + }); + } + } + break; + } + } + } + return found; + }, keywords); +} + +// ─── λ‹€μš΄λ‘œλ“œ λ²„νŠΌ ν…ŒμŠ€νŠΈ ──────────────────────────────────── +async function testDownloadButton(page, btn, target) { + const result = { + buttonText: btn.text, + type: btn.type, + success: false, + fileName: null, + fileSize: null, + error: null, + }; + + try { + // λ‹€μš΄λ‘œλ“œ 이벀트 λŒ€κΈ° μ„€μ • (10초 νƒ€μž„μ•„μ›ƒ) + const downloadPromise = page.waitForEvent('download', { timeout: 10000 }).catch(() => null); + + // API 응닡 κ°μ‹œ (export, download, pdf νŒ¨ν„΄) + const apiPatterns = ['/export', '/download', '/pdf', '.xlsx', '.csv', '.pdf']; + const responsePromise = page.waitForResponse( + resp => apiPatterns.some(p => resp.url().includes(p)), + { timeout: 10000 } + ).catch(() => null); + + // λ²„νŠΌ 클릭 + await page.evaluate((btnIndex) => { + const allBtns = Array.from(document.querySelectorAll('button, a[role="button"], [role="button"]')); + if (allBtns[btnIndex]) allBtns[btnIndex].click(); + }, btn.index); + + // λ‹€μš΄λ‘œλ“œ λ˜λŠ” API 응닡 λŒ€κΈ° + const [download, response] = await Promise.all([downloadPromise, responsePromise]); + + if (download) { + // μ‹€μ œ 파일 λ‹€μš΄λ‘œλ“œ λ°œμƒ + const suggestedName = download.suggestedFilename(); + const savePath = path.join(DOWNLOAD_DIR, `${target.id}_${suggestedName}`); + await download.saveAs(savePath); + + const stat = fs.statSync(savePath); + result.success = true; + result.fileName = suggestedName; + result.fileSize = stat.size; + result.savedPath = savePath; + } else if (response) { + // API 응닡은 μžˆμœΌλ‚˜ λΈŒλΌμš°μ € λ‹€μš΄λ‘œλ“œ μ΄λ²€νŠΈλŠ” μ—†μŒ + const status = response.status(); + const contentType = response.headers()['content-type'] || ''; + + if (status >= 200 && status < 300) { + // Blob λ‹€μš΄λ‘œλ“œ λ“± JSμ—μ„œ μ²˜λ¦¬ν•˜λŠ” 경우 + result.success = true; + result.fileName = `API_${status}_${contentType.split(';')[0]}`; + result.apiStatus = status; + result.contentType = contentType; + } else { + result.error = `API 응닡 ${status}`; + } + } else { + // λ‘˜ λ‹€ μ—†μŒ - μ•Œλ¦Ό/인쇄 νŒμ—…μΌ 수 있음 + // 2초 더 λŒ€κΈ° ν›„ ν† μŠ€νŠΈλ‚˜ μƒνƒœ λ³€ν™” 확인 + await sleep(2000); + const toastOrChange = await page.evaluate(() => { + const toast = document.querySelector('[class*="toast"], [class*="Toastify"], [role="alert"]'); + return toast ? toast.innerText?.trim() : null; + }); + + if (toastOrChange) { + result.error = `λ‹€μš΄λ‘œλ“œ 미감지, ν† μŠ€νŠΈ: "${toastOrChange}"`; + } else { + result.error = 'λ‹€μš΄λ‘œλ“œ 이벀트 및 API 응닡 μ—†μŒ (10초 νƒ€μž„μ•„μ›ƒ)'; + } + } + } catch (e) { + result.error = e.message; + } + + return result; +} + +// ─── λͺ¨λ‹¬ λ‹«κΈ° ────────────────────────────────────────────── +async function closeAllModals(page) { + try { + await page.evaluate(async () => { + for (let i = 0; i < 3; i++) { + const modal = document.querySelector("[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip']), [class*='Modal']"); + if (!modal || modal.getBoundingClientRect().width === 0) break; + + const closeBtn = modal.querySelector("button[class*='close'], [aria-label='λ‹«κΈ°'], [aria-label='Close']") + || Array.from(modal.querySelectorAll('button')).find(b => + ['λ‹«κΈ°', 'Close', 'μ·¨μ†Œ', 'Cancel', '확인'].some(t => b.innerText?.includes(t))); + + if (closeBtn) closeBtn.click(); + else document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true })); + + await new Promise(r => setTimeout(r, 500)); + } + }); + } catch {} +} + +// ─── 리포트 생성 ──────────────────────────────────────────── +function generateReport(results, ts, elapsed, downloadedFiles) { + const passed = results.filter(r => r.status === 'PASS').length; + const partial = results.filter(r => r.status === 'PARTIAL').length; + const failed = results.filter(r => r.status === 'FAIL').length; + const errors = results.filter(r => !['PASS', 'PARTIAL', 'FAIL'].includes(r.status)).length; + const allDownloads = results.flatMap(r => r.downloads); + const dlSuccess = allDownloads.filter(d => d.success).length; + const dlFail = allDownloads.filter(d => !d.success).length; + + let md = `# πŸ“₯ λ‹€μš΄λ‘œλ“œ κΈ°λŠ₯ 검증 리포트 + +**μ‹€ν–‰ μ‹œκ°„**: ${ts.replace('_', ' ')} | **μ†Œμš”**: ${elapsed}초 + +## πŸ“Š μš”μ•½ + +| ν•­λͺ© | κ²°κ³Ό | +|------|------| +| 전체 νŽ˜μ΄μ§€ | ${results.length}개 | +| βœ… PASS | ${passed}개 | +| ⚠️ PARTIAL | ${partial}개 | +| ❌ FAIL | ${failed}개 | +| ⏭️ SKIP/ERROR | ${errors}개 | +| λ‹€μš΄λ‘œλ“œ μ‹œλ„ | ${allDownloads.length}건 | +| λ‹€μš΄λ‘œλ“œ 성곡 | ${dlSuccess}건 | +| λ‹€μš΄λ‘œλ“œ μ‹€νŒ¨ | ${dlFail}건 | + +## πŸ“‹ νŽ˜μ΄μ§€λ³„ κ²°κ³Ό + +| # | νŽ˜μ΄μ§€ | μƒνƒœ | λ‹€μš΄λ‘œλ“œ | λΉ„κ³  | +|---|--------|------|---------|------| +`; + + results.forEach((r, i) => { + const statusIcon = r.status === 'PASS' ? 'βœ…' : r.status === 'PARTIAL' ? '⚠️' : r.status === 'FAIL' ? '❌' : '⏭️'; + const dlCount = r.downloads.length; + const dlOk = r.downloads.filter(d => d.success).length; + const dlInfo = dlCount > 0 ? `${dlOk}/${dlCount}` : '-'; + const note = r.error || r.downloads.filter(d => !d.success).map(d => d.error).join('; ') || ''; + md += `| ${i + 1} | ${r.page} | ${statusIcon} ${r.status} | ${dlInfo} | ${note.substring(0, 60)} |\n`; + }); + + // λ‹€μš΄λ‘œλ“œ 상세 + md += `\n## πŸ“‚ λ‹€μš΄λ‘œλ“œ 상세 κ²°κ³Ό\n\n`; + + for (const r of results) { + if (r.downloads.length === 0) continue; + md += `### ${r.page}\n\n`; + md += `| λ²„νŠΌ | μœ ν˜• | κ²°κ³Ό | 파일λͺ… | 크기 | λΉ„κ³  |\n`; + md += `|------|------|------|--------|------|------|\n`; + + for (const d of r.downloads) { + const icon = d.success ? 'βœ…' : '❌'; + const size = d.fileSize ? `${(d.fileSize / 1024).toFixed(1)}KB` : '-'; + const note = d.error || ''; + md += `| ${d.buttonText} | ${d.type} | ${icon} | ${d.fileName || '-'} | ${size} | ${note} |\n`; + } + md += '\n'; + } + + // λ‹€μš΄λ‘œλ“œλœ 파일 λͺ©λ‘ + if (downloadedFiles.length > 0) { + md += `## πŸ“ λ‹€μš΄λ‘œλ“œλœ 파일 λͺ©λ‘\n\n`; + md += `| # | 파일λͺ… | 크기 |\n`; + md += `|---|--------|------|\n`; + downloadedFiles.forEach((d, i) => { + const size = d.fileSize ? `${(d.fileSize / 1024).toFixed(1)}KB` : '-'; + md += `| ${i + 1} | ${d.fileName} | ${size} |\n`; + }); + md += '\n'; + } + + // μ‹€νŒ¨ ν•­λͺ© 상세 + const failItems = results.filter(r => !['PASS'].includes(r.status)); + if (failItems.length > 0) { + md += `## ❌ μ‹€νŒ¨/이슈 ν•­λͺ©\n\n`; + for (const r of failItems) { + md += `- **${r.page}**: ${r.status} - ${r.error || r.downloads.filter(d => !d.success).map(d => `${d.buttonText}: ${d.error}`).join(', ')}\n`; + } + } + + return md; +} + +// ─── Run ──────────────────────────────────────────────────── +main().catch(err => { + console.error(C.red(`\nπŸ’₯ 치λͺ…적 였λ₯˜: ${err.message}`)); + process.exit(1); +});