From e318c5e042a8b9d21a689909111fdd49e6ed9384 Mon Sep 17 00:00:00 2001 From: javamon1174 Date: Wed, 25 Mar 2026 15:18:57 +0900 Subject: [PATCH] =?UTF-8?q?Playwright=20E2E=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=99=98=EA=B2=BD=20=EA=B5=AC=EC=84=B1=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=84=B0=20=EA=B3=84=EC=A0=95=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Playwright + Chromium 브라우저 테스트 환경 세팅 - 테스터 계정 4개 생성 (admin/local/shop/user, pw: test1234!) - seed SQL + Node.js 시더 스크립트 포함 - E2E 테스트 23개 작성 (전체 통과): - auth: 로그인/로그아웃/실패/회원가입 (9개) - admin: 지자체관리자/Super Admin 패널 접근 (10개) - public: 홈/로그인/회원가입/404 (4개) - CLAUDE.md: 테스트 섹션을 Playwright 기반으로 업데이트 - jobs.md: 테스트 작업 완료 기록 Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 8 + CLAUDE.md | 48 +++-- e2e/admin.spec.js | 83 ++++++++ e2e/auth.spec.js | 70 +++++++ e2e/helpers/auth.js | 35 ++++ e2e/helpers/db-seed.js | 68 +++++++ e2e/public.spec.js | 25 +++ jobs.md | 4 + package-lock.json | 245 +++++++++++++++++++++++ package.json | 20 ++ playwright.config.js | 26 +++ writable/database/seed_test_accounts.sql | 32 +++ 12 files changed, 649 insertions(+), 15 deletions(-) create mode 100644 e2e/admin.spec.js create mode 100644 e2e/auth.spec.js create mode 100644 e2e/helpers/auth.js create mode 100644 e2e/helpers/db-seed.js create mode 100644 e2e/public.spec.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.js create mode 100644 writable/database/seed_test_accounts.sql diff --git a/.gitignore b/.gitignore index 23a1e2b..c9269d7 100644 --- a/.gitignore +++ b/.gitignore @@ -163,5 +163,13 @@ _modules/* # Claude Code .claude/ +# Node.js +node_modules/ + +# Playwright +playwright-report/ +test-results/ +blob-report/ + /results/ /phpunit*.xml diff --git a/CLAUDE.md b/CLAUDE.md index 1ba1975..b887f9a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,31 +73,49 @@ assets/ # 기획 문서 (엑셀) php spark serve --port=8045 ``` -## 테스트 +## 테스트 (Playwright E2E) -모든 작업 완료 후 반드시 테스트를 수행합니다. +모든 작업 완료 후 반드시 Playwright 브라우저 테스트를 수행합니다. ```bash # 전체 테스트 -vendor/bin/phpunit +npm test -# 특정 테스트 스위트 -vendor/bin/phpunit --testsuite App +# headed 모드 (브라우저 표시) +npm run test:headed -# 특정 테스트 파일 -vendor/bin/phpunit tests/unit/SomeTest.php +# 특정 파일만 +npx playwright test e2e/auth.spec.js -# 필터로 특정 메서드만 -vendor/bin/phpunit --filter testMethodName +# 특정 테스트만 +npx playwright test -g "로그인 페이지" ``` +### 테스트 구조 +``` +e2e/ +├── helpers/ +│ ├── auth.js # 로그인/로그아웃 헬퍼, 테스터 계정 정보 +│ └── db-seed.js # 테스터 계정 DB 시딩 스크립트 +├── auth.spec.js # 인증 테스트 (로그인/로그아웃/회원가입) +├── admin.spec.js # 관리자 패널 테스트 (지자체관리자/Super Admin) +└── public.spec.js # 공개 페이지 테스트 +``` + +### 테스터 계정 (비밀번호: `test1234!`) +| ID | 역할 | Level | +|----|------|-------| +| `tester_admin` | Super Admin | 4 | +| `tester_local` | 지자체관리자 (대구) | 3 | +| `tester_shop` | 지정판매소 | 2 | +| `tester_user` | 일반 사용자 | 1 | + ### 테스트 규칙 -- **작업 완료 시 해당 기능의 테스트를 반드시 작성/실행** -- 테스트 디렉토리: `tests/unit/`, `tests/database/`, `tests/session/` -- CI4 TestCase 사용: `CodeIgniter\Test\CIUnitTestCase`, `CodeIgniter\Test\DatabaseTestTrait` -- 테스트 파일명: `{ClassName}Test.php` (PascalCase) -- 테스트 메서드명: `test{행위}` (camelCase) -- DB 테스트 시 `DatabaseTestTrait` + `$seed` 속성 활용 +- **작업 완료 시 해당 기능의 E2E 테스트를 반드시 작성/실행** +- 테스트 파일: `e2e/{기능명}.spec.js` +- 공통 로그인은 `e2e/helpers/auth.js`의 `login(page, role)` 사용 +- 새 기능 추가 시 해당 페이지 접근 + 주요 CRUD 동작 테스트 작성 +- 테스터 계정 시딩: `node e2e/helpers/db-seed.js` ## DB 초기화 순서 diff --git a/e2e/admin.spec.js b/e2e/admin.spec.js new file mode 100644 index 0000000..72c0b15 --- /dev/null +++ b/e2e/admin.spec.js @@ -0,0 +1,83 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); +const { login } = require('./helpers/auth'); + +test.describe('관리자 패널 — 지자체관리자', () => { + + test.beforeEach(async ({ page }) => { + await login(page, 'local'); + }); + + test('관리자 대시보드 접근', async ({ page }) => { + await page.goto('/admin'); + await expect(page).toHaveURL(/\/admin/); + }); + + test('회원 관리 목록 접근', async ({ page }) => { + await page.goto('/admin/users'); + await expect(page).toHaveURL(/\/admin\/users/); + const content = await page.content(); + expect(content).toContain('tester_'); + }); + + test('로그인 이력 접근', async ({ page }) => { + await page.goto('/admin/access/login-history'); + await expect(page).toHaveURL(/\/admin\/access\/login-history/); + }); + + test('승인 대기 목록 접근', async ({ page }) => { + await page.goto('/admin/access/approvals'); + await expect(page).toHaveURL(/\/admin\/access\/approvals/); + }); + + test('역할 목록 접근', async ({ page }) => { + await page.goto('/admin/roles'); + await expect(page).toHaveURL(/\/admin\/roles/); + }); + + test('메뉴 관리 접근', async ({ page }) => { + await page.goto('/admin/menus'); + await expect(page).toHaveURL(/\/admin\/menus/); + }); + + test('지정판매소 목록 접근', async ({ page }) => { + await page.goto('/admin/designated-shops'); + await expect(page).toHaveURL(/\/admin\/designated-shops/); + }); + + test('지자체 관리는 Super Admin 전용 — 지자체관리자 접근 시 리다이렉트', async ({ page }) => { + await page.goto('/admin/local-governments'); + // Level 3는 Super Admin이 아니므로 /admin으로 리다이렉트됨 + await expect(page).toHaveURL(/\/admin$/); + }); +}); + +test.describe('관리자 패널 — Super Admin', () => { + + test('지자체 선택 후 관리자 접근', async ({ page }) => { + await login(page, 'admin'); + await expect(page).toHaveURL(/\/admin\/select-local-government/); + + // radio 버튼으로 첫 번째 지자체 선택 + const radio = page.locator('input[name="lg_idx"]').first(); + await radio.check(); + await page.click('button[type="submit"]'); + + // 선택 후 관리자 대시보드로 이동 + await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 }); + await page.goto('/admin'); + await expect(page).not.toHaveURL(/\/select-local-government/); + }); + + test('Super Admin은 지자체 관리 접근 가능', async ({ page }) => { + // 먼저 로그인 + 지자체 선택 + await login(page, 'admin'); + const radio = page.locator('input[name="lg_idx"]').first(); + await radio.check(); + await page.click('button[type="submit"]'); + await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 }); + + await page.goto('/admin/local-governments'); + await expect(page).toHaveURL(/\/admin\/local-governments/); + }); +}); diff --git a/e2e/auth.spec.js b/e2e/auth.spec.js new file mode 100644 index 0000000..a30d178 --- /dev/null +++ b/e2e/auth.spec.js @@ -0,0 +1,70 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); +const { login, logout, TEST_ACCOUNTS } = require('./helpers/auth'); + +test.describe('인증 시스템', () => { + + test('로그인 페이지 접속', async ({ page }) => { + await page.goto('/login'); + await expect(page).toHaveURL(/\/login/); + await expect(page.locator('input[name="login_id"]')).toBeVisible(); + await expect(page.locator('input[name="password"]')).toBeVisible(); + }); + + test('잘못된 ID로 로그인 실패', async ({ page }) => { + await page.goto('/login'); + await page.fill('input[name="login_id"]', 'wrong_user'); + await page.fill('input[name="password"]', 'wrong_pass'); + await page.click('button[type="submit"]'); + // 로그인 페이지에 머물러야 함 + await expect(page).toHaveURL(/\/login/); + }); + + test('잘못된 비밀번호로 로그인 실패', async ({ page }) => { + await page.goto('/login'); + await page.fill('input[name="login_id"]', 'tester_admin'); + await page.fill('input[name="password"]', 'wrong_password'); + await page.click('button[type="submit"]'); + await expect(page).toHaveURL(/\/login/); + }); + + test('Super Admin 로그인 성공 → 지자체 선택 페이지', async ({ page }) => { + await login(page, 'admin'); + // Super Admin은 지자체 선택 페이지로 리다이렉트 + await expect(page).toHaveURL(/\/admin\/select-local-government/); + }); + + test('지자체관리자 로그인 성공 → 관리자 대시보드', async ({ page }) => { + await login(page, 'local'); + await expect(page).toHaveURL(/\/admin/); + }); + + test('일반 사용자 로그인 성공 → 홈/대시보드', async ({ page }) => { + await login(page, 'user'); + // 일반 사용자는 홈 또는 대시보드로 이동 + const url = page.url(); + expect(url.includes('/dashboard') || url.endsWith('/') || url.includes('/login') === false).toBeTruthy(); + }); + + test('로그아웃', async ({ page }) => { + await login(page, 'local'); + await page.goto('/logout'); + await page.waitForURL(/\/login/, { timeout: 10000 }); + await expect(page).toHaveURL(/\/login/); + // 로그아웃 후 관리자 접근 불가 확인 + await page.goto('/admin'); + await expect(page).toHaveURL(/\/login/); + }); + + test('비로그인 상태에서 관리자 페이지 접근 → 로그인으로 리다이렉트', async ({ page }) => { + await page.goto('/admin'); + await expect(page).toHaveURL(/\/login/); + }); + + test('회원가입 페이지 접속', async ({ page }) => { + await page.goto('/register'); + await expect(page.locator('input[name="mb_id"]')).toBeVisible(); + await expect(page.locator('input[name="mb_passwd"]')).toBeVisible(); + await expect(page.locator('input[name="mb_name"]')).toBeVisible(); + }); +}); diff --git a/e2e/helpers/auth.js b/e2e/helpers/auth.js new file mode 100644 index 0000000..3c1355b --- /dev/null +++ b/e2e/helpers/auth.js @@ -0,0 +1,35 @@ +/** + * 공통 인증 헬퍼 + */ + +const TEST_ACCOUNTS = { + admin: { id: 'tester_admin', password: 'test1234!', level: 4 }, + local: { id: 'tester_local', password: 'test1234!', level: 3 }, + shop: { id: 'tester_shop', password: 'test1234!', level: 2 }, + user: { id: 'tester_user', password: 'test1234!', level: 1 }, +}; + +/** + * 로그인 수행 + * @param {import('@playwright/test').Page} page + * @param {'admin'|'local'|'shop'|'user'} role + */ +async function login(page, role = 'admin') { + const acct = TEST_ACCOUNTS[role]; + await page.goto('/login'); + await page.fill('input[name="login_id"]', acct.id); + await page.fill('input[name="password"]', acct.password); + await page.click('button[type="submit"]'); + // 로그인 후 리다이렉트 대기 + await page.waitForURL(url => !url.pathname.includes('/login'), { timeout: 10000 }); +} + +/** + * 로그아웃 수행 + * @param {import('@playwright/test').Page} page + */ +async function logout(page) { + await page.goto('/logout'); +} + +module.exports = { TEST_ACCOUNTS, login, logout }; diff --git a/e2e/helpers/db-seed.js b/e2e/helpers/db-seed.js new file mode 100644 index 0000000..4e2b99a --- /dev/null +++ b/e2e/helpers/db-seed.js @@ -0,0 +1,68 @@ +/** + * DB 시더: 테스터 계정 생성 + * 실행: node e2e/helpers/db-seed.js + */ +const mysql = require('mysql2/promise'); +const bcrypt = require('bcryptjs'); + +const DB_CONFIG = { + host: '3.36.27.239', + port: 3306, + user: 'root', + password: 'ssadm!#@$', + database: 'jongryangje_dev', +}; + +const TEST_PASSWORD = 'test1234!'; + +const TEST_ACCOUNTS = [ + { id: 'tester_admin', name: '테스터관리자', email: 'tester_admin@test.com', phone: '010-0000-0001', level: 4, lg_idx: null }, + { id: 'tester_local', name: '테스터지자체', email: 'tester_local@test.com', phone: '010-0000-0002', level: 3, lg_idx: 1 }, + { id: 'tester_shop', name: '테스터판매소', email: 'tester_shop@test.com', phone: '010-0000-0003', level: 2, lg_idx: null }, + { id: 'tester_user', name: '테스터사용자', email: 'tester_user@test.com', phone: '010-0000-0004', level: 1, lg_idx: null }, +]; + +async function seed() { + const conn = await mysql.createConnection(DB_CONFIG); + const hash = bcrypt.hashSync(TEST_PASSWORD, 10).replace('$2b$', '$2y$'); + + console.log('테스터 계정 시딩 시작...'); + + for (const acct of TEST_ACCOUNTS) { + // Upsert member + await conn.execute( + `INSERT INTO member (mb_id, mb_passwd, mb_name, mb_email, mb_phone, mb_lang, mb_level, mb_group, mb_lg_idx, mb_state, mb_regdate) + VALUES (?, ?, ?, ?, ?, 'ko', ?, '', ?, 1, NOW()) + ON DUPLICATE KEY UPDATE mb_passwd = VALUES(mb_passwd), mb_state = 1, mb_level = VALUES(mb_level)`, + [acct.id, hash, acct.name, acct.email, acct.phone, acct.level, acct.lg_idx] + ); + + // Ensure approval request exists (approved) + const [rows] = await conn.execute( + 'SELECT mb_idx FROM member WHERE mb_id = ?', [acct.id] + ); + if (rows.length > 0) { + const mbIdx = rows[0].mb_idx; + const [existing] = await conn.execute( + 'SELECT mar_idx FROM member_approval_request WHERE mb_idx = ?', [mbIdx] + ); + if (existing.length === 0) { + await conn.execute( + `INSERT INTO member_approval_request (mb_idx, mar_requested_level, mar_status, mar_request_note, mar_requested_at, mar_processed_at) + VALUES (?, ?, 'approved', '테스트 계정 자동 승인', NOW(), NOW())`, + [mbIdx, acct.level] + ); + } + } + + console.log(` ✓ ${acct.id} (Level ${acct.level})`); + } + + await conn.end(); + console.log('시딩 완료!'); +} + +seed().catch(err => { + console.error('시딩 실패:', err.message); + process.exit(1); +}); diff --git a/e2e/public.spec.js b/e2e/public.spec.js new file mode 100644 index 0000000..944944b --- /dev/null +++ b/e2e/public.spec.js @@ -0,0 +1,25 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); + +test.describe('공개 페이지', () => { + + test('홈페이지 접근', async ({ page }) => { + const response = await page.goto('/'); + expect(response?.status()).toBe(200); + }); + + test('로그인 페이지 접근', async ({ page }) => { + const response = await page.goto('/login'); + expect(response?.status()).toBe(200); + }); + + test('회원가입 페이지 접근', async ({ page }) => { + const response = await page.goto('/register'); + expect(response?.status()).toBe(200); + }); + + test('존재하지 않는 페이지 → 404', async ({ page }) => { + const response = await page.goto('/nonexistent-page-12345'); + expect(response?.status()).toBe(404); + }); +}); diff --git a/jobs.md b/jobs.md index 1167e2b..7675694 100644 --- a/jobs.md +++ b/jobs.md @@ -144,6 +144,7 @@ | INIT-09 | 지정판매소 관리 CRUD | 2026-01 | `4e557d4` | | INIT-10 | PII 암호화 (이메일/전화번호) | 2026-01 | `4e557d4` | | DOC-01 | README 개발현황 정리 + CLAUDE.md | 2026-03-25 | `c07261a` | +| TEST-01 | Playwright E2E 테스트 환경 + 테스터 계정 + 23개 테스트 | 2026-03-25 | — | --- @@ -153,6 +154,9 @@ ### 2026-03-25 +- **TEST-01** Playwright E2E 테스트 환경 구성 (Chromium) +- **TEST-01** 테스터 계정 4개 생성 (admin/local/shop/user, 비밀번호: test1234!) +- **TEST-01** E2E 테스트 23개 작성 및 전체 통과 (auth 9, admin 10, public 4) - **DOC-01** README.md 개발현황 상세 정리 (63개 웹 + 15개 앱 항목별 분석) - **DOC-01** CLAUDE.md 생성 (Claude Code 프로젝트 가이드) - **DOC-01** jobs.md 생성 (작업 관리 파일) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..666b082 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,245 @@ +{ + "name": "jongryangje", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "jongryangje", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.58.2", + "bcryptjs": "^3.0.3", + "mysql2": "^3.20.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "dev": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/mysql2": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.20.0.tgz", + "integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "dev": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a027862 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "jongryangje", + "version": "1.0.0", + "description": "종량제 쓰레기봉투 물류시스템", + "scripts": { + "test": "npx playwright test", + "test:ui": "npx playwright test --ui", + "test:headed": "npx playwright test --headed" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wixon-associates/jongryangje.git" + }, + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.58.2", + "bcryptjs": "^3.0.3", + "mysql2": "^3.20.0" + } +} diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..ca96c71 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,26 @@ +// @ts-check +const { defineConfig } = require('@playwright/test'); + +module.exports = defineConfig({ + testDir: './e2e', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: [['html', { open: 'never' }], ['list']], + timeout: 30000, + + use: { + baseURL: 'http://localhost:8045', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + locale: 'ko-KR', + }, + + projects: [ + { + name: 'chromium', + use: { browserName: 'chromium' }, + }, + ], +}); diff --git a/writable/database/seed_test_accounts.sql b/writable/database/seed_test_accounts.sql new file mode 100644 index 0000000..46416d4 --- /dev/null +++ b/writable/database/seed_test_accounts.sql @@ -0,0 +1,32 @@ +-- ============================================ +-- 테스터 계정 시드 데이터 +-- 비밀번호: test1234! (bcrypt hash) +-- 실행: mysql -u root -p jongryangje_dev < seed_test_accounts.sql +-- ============================================ + +-- Super Admin (Level 4) +INSERT INTO `member` (`mb_id`, `mb_passwd`, `mb_name`, `mb_email`, `mb_phone`, `mb_lang`, `mb_level`, `mb_group`, `mb_lg_idx`, `mb_state`, `mb_regdate`) +VALUES ('tester_admin', '$2y$10$t8FlP9uqDux942Chm1WO3uFhJ5M9G8O9inY20rwTRgLru2ae.t.xS', '테스터관리자', 'tester_admin@test.com', '010-0000-0001', 'ko', 4, '', NULL, 1, NOW()) +ON DUPLICATE KEY UPDATE `mb_passwd` = VALUES(`mb_passwd`), `mb_state` = 1; + +-- 지자체관리자 (Level 3) — lg_idx=1 (대구) +INSERT INTO `member` (`mb_id`, `mb_passwd`, `mb_name`, `mb_email`, `mb_phone`, `mb_lang`, `mb_level`, `mb_group`, `mb_lg_idx`, `mb_state`, `mb_regdate`) +VALUES ('tester_local', '$2y$10$t8FlP9uqDux942Chm1WO3uFhJ5M9G8O9inY20rwTRgLru2ae.t.xS', '테스터지자체', 'tester_local@test.com', '010-0000-0002', 'ko', 3, '', 1, 1, NOW()) +ON DUPLICATE KEY UPDATE `mb_passwd` = VALUES(`mb_passwd`), `mb_state` = 1; + +-- 지정판매소 (Level 2) +INSERT INTO `member` (`mb_id`, `mb_passwd`, `mb_name`, `mb_email`, `mb_phone`, `mb_lang`, `mb_level`, `mb_group`, `mb_lg_idx`, `mb_state`, `mb_regdate`) +VALUES ('tester_shop', '$2y$10$t8FlP9uqDux942Chm1WO3uFhJ5M9G8O9inY20rwTRgLru2ae.t.xS', '테스터판매소', 'tester_shop@test.com', '010-0000-0003', 'ko', 2, '', NULL, 1, NOW()) +ON DUPLICATE KEY UPDATE `mb_passwd` = VALUES(`mb_passwd`), `mb_state` = 1; + +-- 일반 사용자 (Level 1) +INSERT INTO `member` (`mb_id`, `mb_passwd`, `mb_name`, `mb_email`, `mb_phone`, `mb_lang`, `mb_level`, `mb_group`, `mb_lg_idx`, `mb_state`, `mb_regdate`) +VALUES ('tester_user', '$2y$10$t8FlP9uqDux942Chm1WO3uFhJ5M9G8O9inY20rwTRgLru2ae.t.xS', '테스터사용자', 'tester_user@test.com', '010-0000-0004', 'ko', 1, '', NULL, 1, NOW()) +ON DUPLICATE KEY UPDATE `mb_passwd` = VALUES(`mb_passwd`), `mb_state` = 1; + +-- 승인 요청도 approved 상태로 생성 (tester 계정이 정상 로그인 가능하도록) +INSERT INTO `member_approval_request` (`mb_idx`, `mar_requested_level`, `mar_status`, `mar_request_note`, `mar_requested_at`, `mar_processed_at`) +SELECT mb_idx, mb_level, 'approved', '테스트 계정 자동 승인', NOW(), NOW() +FROM `member` +WHERE `mb_id` IN ('tester_admin', 'tester_local', 'tester_shop', 'tester_user') +AND `mb_idx` NOT IN (SELECT mb_idx FROM `member_approval_request`);