diff --git a/.gitea/workflows/code_check.yml b/.gitea/workflows/code_check.yml
index 67679a6b..32f058a9 100644
--- a/.gitea/workflows/code_check.yml
+++ b/.gitea/workflows/code_check.yml
@@ -334,8 +334,7 @@ jobs:
- name: Get Playwright version
id: playwright-version
run: |
- cd userfront-e2e
- echo "version=$(npm list @playwright/test | grep @playwright/test | awk -F@ '{print $NF}' | head -n 1)" >> "$GITHUB_OUTPUT"
+ node scripts/playwrightPackageVersion.cjs userfront-e2e >> "$GITHUB_OUTPUT"
- name: Cache Playwright Browsers
uses: actions/cache@v4
@@ -563,8 +562,7 @@ jobs:
- name: Get Playwright version
id: playwright-version
run: |
- cd adminfront
- echo "version=$(pnpm list -C ../common @playwright/test --depth 0 | grep @playwright/test | awk -F@ '{print $NF}' | head -n 1)" >> "$GITHUB_OUTPUT"
+ node scripts/playwrightPackageVersion.cjs adminfront >> "$GITHUB_OUTPUT"
- name: Cache Playwright Browsers
uses: actions/cache@v4
@@ -657,7 +655,7 @@ jobs:
id: playwright-version
working-directory: devfront
run: |
- echo "version=$(pnpm list -C ../common @playwright/test --depth 0 | grep @playwright/test | awk -F@ '{print $NF}' | head -n 1)" >> "$GITHUB_OUTPUT"
+ node ../scripts/playwrightPackageVersion.cjs . >> "$GITHUB_OUTPUT"
- name: Cache Playwright Browsers
uses: actions/cache@v4
@@ -834,8 +832,7 @@ jobs:
- name: Get Playwright version
id: playwright-version
run: |
- cd orgfront
- echo "version=$(pnpm list -C ../common @playwright/test --depth 0 | grep @playwright/test | awk -F@ '{print $NF}' | head -n 1)" >> "$GITHUB_OUTPUT"
+ node scripts/playwrightPackageVersion.cjs orgfront >> "$GITHUB_OUTPUT"
- name: Cache Playwright Browsers
uses: actions/cache@v4
diff --git a/adminfront/src/features/users/UserListPage.tsx b/adminfront/src/features/users/UserListPage.tsx
index c24bede1..6c63cc1d 100644
--- a/adminfront/src/features/users/UserListPage.tsx
+++ b/adminfront/src/features/users/UserListPage.tsx
@@ -517,8 +517,7 @@ function UserListPage() {
{
- e.preventDefault();
+ onSelect={() => {
setBulkUploadOpen(true);
}}
className="cursor-pointer"
diff --git a/adminfront/src/locales/en.toml b/adminfront/src/locales/en.toml
index b3228ecd..6dea4489 100644
--- a/adminfront/src/locales/en.toml
+++ b/adminfront/src/locales/en.toml
@@ -1279,6 +1279,7 @@ type = "TYPE"
updated = "UPDATED"
[ui.admin.users]
+csv_template = "Download Template"
[ui.admin.users.bulk]
create_missing_tenant = "Create new"
diff --git a/adminfront/src/locales/ko.toml b/adminfront/src/locales/ko.toml
index e34691a0..650a07b6 100644
--- a/adminfront/src/locales/ko.toml
+++ b/adminfront/src/locales/ko.toml
@@ -1282,6 +1282,7 @@ type = "유형"
updated = "UPDATED"
[ui.admin.users]
+csv_template = "템플릿 다운로드"
[ui.admin.users.bulk]
create_missing_tenant = "신규 생성"
diff --git a/adminfront/src/locales/template.toml b/adminfront/src/locales/template.toml
index 85b2adbd..321704b6 100644
--- a/adminfront/src/locales/template.toml
+++ b/adminfront/src/locales/template.toml
@@ -1294,6 +1294,7 @@ type = ""
updated = ""
[ui.admin.users]
+csv_template = ""
[ui.admin.users.bulk]
create_missing_tenant = ""
diff --git a/adminfront/tests/auth.spec.ts b/adminfront/tests/auth.spec.ts
index 5b7a9a65..3aee9964 100644
--- a/adminfront/tests/auth.spec.ts
+++ b/adminfront/tests/auth.spec.ts
@@ -106,7 +106,7 @@ test.describe("Authentication", () => {
await page.goto("/");
await expect(page.getByRole("link", { name: "조직도" })).toHaveAttribute(
"href",
- "http://localhost:5175/login?auto=1&returnTo=%2Fchart",
+ "http://localhost:5175/login?auto=1&returnTo=%2Fchart%3FincludeInternal%3Dtrue",
);
});
diff --git a/adminfront/tests/users.spec.ts b/adminfront/tests/users.spec.ts
index e2971245..41ca6784 100644
--- a/adminfront/tests/users.spec.ts
+++ b/adminfront/tests/users.spec.ts
@@ -423,7 +423,10 @@ test.describe("User Management", () => {
const [download] = await Promise.all([
page.waitForEvent("download"),
- page.getByTestId("user-export-without-ids-btn").click(),
+ (async () => {
+ await page.getByTestId("user-data-mgmt-btn").click();
+ await page.getByTestId("user-export-menu-item").click();
+ })(),
]);
expect(download.suggestedFilename()).toBe("users.csv");
diff --git a/adminfront/tests/users_bulk.spec.ts b/adminfront/tests/users_bulk.spec.ts
index 10346d57..2418585c 100644
--- a/adminfront/tests/users_bulk.spec.ts
+++ b/adminfront/tests/users_bulk.spec.ts
@@ -66,8 +66,10 @@ test.describe("Users Bulk Upload", () => {
{ timeout: 20000 },
);
- const bulkBtn = page.getByTestId("bulk-import-btn");
- await bulkBtn.click();
+ await page.getByTestId("user-data-mgmt-btn").click();
+ await page
+ .getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i })
+ .click();
await expect(page.getByTestId("bulk-upload-title")).toBeVisible({
timeout: 10000,
@@ -106,8 +108,10 @@ test.describe("Users Bulk Upload", () => {
{ timeout: 20000 },
);
- const bulkBtn = page.getByTestId("bulk-import-btn");
- await bulkBtn.click();
+ await page.getByTestId("user-data-mgmt-btn").click();
+ await page
+ .getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i })
+ .click();
const uploadBtn = page.getByTestId("bulk-start-btn");
await expect(uploadBtn).toBeDisabled();
@@ -168,7 +172,10 @@ test.describe("Users Bulk Upload", () => {
{ timeout: 20000 },
);
- await page.getByTestId("bulk-import-btn").click();
+ await page.getByTestId("user-data-mgmt-btn").click();
+ await page
+ .getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i })
+ .click();
await page.locator('input[type="file"]').setInputFiles({
name: "users.csv",
mimeType: "text/csv",
@@ -274,7 +281,10 @@ test.describe("Users Bulk Upload", () => {
{ timeout: 20000 },
);
- await page.getByTestId("bulk-import-btn").click();
+ await page.getByTestId("user-data-mgmt-btn").click();
+ await page
+ .getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i })
+ .click();
await page.locator('input[type="file"]').setInputFiles({
name: "users.csv",
mimeType: "text/csv",
diff --git a/locales/en.toml b/locales/en.toml
index 09e18f0a..b79c7a3f 100644
--- a/locales/en.toml
+++ b/locales/en.toml
@@ -1428,6 +1428,7 @@ updated = "UPDATED"
created = "CREATED"
[ui.admin.users]
+csv_template = "Download Template"
[ui.admin.users.bulk]
acknowledge_warning = "I acknowledge the warning and will proceed."
diff --git a/locales/ko.toml b/locales/ko.toml
index 7b6e5beb..0a34eb96 100644
--- a/locales/ko.toml
+++ b/locales/ko.toml
@@ -1892,6 +1892,7 @@ created = "CREATED"
created = "CREATED"
[ui.admin.users]
+csv_template = "템플릿 다운로드"
[ui.admin.users.bulk]
acknowledge_warning = "경고를 확인했으며 계속 진행합니다."
diff --git a/locales/template.toml b/locales/template.toml
index 0e5745b3..8180d68d 100644
--- a/locales/template.toml
+++ b/locales/template.toml
@@ -1771,6 +1771,7 @@ updated = ""
created = ""
[ui.admin.users]
+csv_template = ""
[ui.admin.users.bulk]
acknowledge_warning = ""
diff --git a/scripts/playwrightPackageVersion.cjs b/scripts/playwrightPackageVersion.cjs
new file mode 100644
index 00000000..9ad5e0c9
--- /dev/null
+++ b/scripts/playwrightPackageVersion.cjs
@@ -0,0 +1,24 @@
+#!/usr/bin/env node
+
+const fs = require("node:fs");
+const path = require("node:path");
+
+const packageDir = process.argv[2];
+
+if (!packageDir) {
+ console.error("Usage: node scripts/playwrightPackageVersion.cjs ");
+ process.exit(2);
+}
+
+const packageJsonPath = path.resolve(process.cwd(), packageDir, "package.json");
+const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
+const version =
+ packageJson.devDependencies?.["@playwright/test"] ??
+ packageJson.dependencies?.["@playwright/test"];
+
+if (!version) {
+ console.error(`@playwright/test version not found in ${packageJsonPath}`);
+ process.exit(1);
+}
+
+console.log(`version=${version.replace(/^[^0-9]*/, "")}`);
diff --git a/test/code_check_playwright_cache_policy_test.sh b/test/code_check_playwright_cache_policy_test.sh
new file mode 100644
index 00000000..433cacf4
--- /dev/null
+++ b/test/code_check_playwright_cache_policy_test.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+WORKFLOW_FILE="$ROOT_DIR/.gitea/workflows/code_check.yml"
+
+fail() {
+ echo "ERROR: $*" >&2
+ exit 1
+}
+
+grep -Fq -- "scripts/playwrightPackageVersion.cjs" "$WORKFLOW_FILE" || \
+ fail "Code Check workflow must compute Playwright cache versions through the shared package.json reader"
+
+if grep -Fq -- "npm list @playwright/test" "$WORKFLOW_FILE"; then
+ fail "Code Check workflow must not call npm list before npm ci"
+fi
+
+if grep -Fq -- "pnpm list -C ../common @playwright/test" "$WORKFLOW_FILE"; then
+ fail "Code Check workflow must not call pnpm list before pnpm install"
+fi
+
+echo "OK: Code Check Playwright cache keys do not depend on preinstalled node_modules"
diff --git a/test/playwright_package_version_test.sh b/test/playwright_package_version_test.sh
new file mode 100644
index 00000000..c6b4e319
--- /dev/null
+++ b/test/playwright_package_version_test.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+fail() {
+ echo "ERROR: $*" >&2
+ exit 1
+}
+
+assert_version() {
+ local package_dir="$1"
+ local expected="$2"
+ local actual
+ actual="$(node "$ROOT_DIR/scripts/playwrightPackageVersion.cjs" "$package_dir")"
+ [ "$actual" = "version=$expected" ] || \
+ fail "$package_dir Playwright version must be $expected, got: $actual"
+}
+
+assert_version userfront-e2e 1.58.2
+assert_version adminfront 1.60.0
+assert_version devfront 1.60.0
+assert_version orgfront 1.60.0
+
+echo "OK: Playwright package versions are read from package.json"