diff --git a/packages/wiki-shared/src/i18n/en.json b/packages/wiki-shared/src/i18n/en.json
index abcb013..08fc260 100644
--- a/packages/wiki-shared/src/i18n/en.json
+++ b/packages/wiki-shared/src/i18n/en.json
@@ -628,7 +628,7 @@
"m_eba914ae07bfbadf": "Unable to load SVG sanitizer library. Please refresh the page and try again.",
"m_16a254e99098adf0": "Spaces will - be converted, and leading/trailing spaces will be- removed. ",
"m_5d704cd6c4bfa10f": "< > ^ / \\ . ? Cannot be used.",
- "m_a1f6b16b2c3b2a38": "\n Tags (optional) \n
\n \n
\n Only Korean/English letters, numbers, spaces, _, ., - are allowed · up to 20
",
+ "m_a1f6b16b2c3b2a38": "\n Tags (optional) \n \n \n
\n Only Chinese/Korean/English letters, numbers, spaces, _, ., - are allowed · up to 20
",
"m_ba18fae2953eebf1": "\n \n
File name (excluding extension) \n
\n
__VIREO_SLOT_1__
\n __VIREO_SLOT_2__\n
\n ",
"m_0a1ac7eec7b927f7": "Upload",
"m_e6481000e178501b": "Saved file name: __VIREO_SLOT_0__",
@@ -1406,7 +1406,7 @@
"m_00bbefb0317222b2": "You do not have permission to perform this action.",
"m_fcfc6676d4778b53": "Edit image page",
"m_a3030bf8f16dc63c": "Save",
- "m_cbadc087b51a5373": "\n \n
Description \n
\n
Tag \n
\n \n
\n
Only Korean/English letters, numbers, spaces, _, ., - are allowed · up to 20
\n
\n ",
+ "m_cbadc087b51a5373": "\n \n
Description \n
\n
Tag \n
\n \n
\n
Only Chinese/Korean/English letters, numbers, spaces, _, ., - are allowed · up to 20
\n
\n ",
"m_193f229df3d99e3a": "Saved.",
"m_ac66748e68a838f4": "Document does not exist",
"m_58d547bd560c8cb9": "\n \n Linked pages\n \n \n View document structure\n \n ",
@@ -1878,7 +1878,7 @@
"m_41e7be2dde07e3b9": "\n \n Permission \n \n __VIREO_SLOT_2__\n
\n Membership threshold (N days) is in Admin Console > Wiki Settings Edit ACL membership days Follows global settings. Admin only Flag blocks general user edits in bulk. \n \n ",
"m_8edcb1517374d55f": "\n \n \n \n Private (admin only view) \n \n
\n \n Edit ACL \n Leave as is \n Disable \n Apply ACL below \n
\n __VIREO_SLOT_3__\n \n \n \n Save to current document\n \n
\n ",
"m_c5ac0714fceb948d": "Document metadata load failed",
- "m_0c53e3964dd0d9de": "\n \n
\n \n Current document \n Apply to this document only \n \n __VIREO_SLOT_0__\n \n\n
\n \n Apply to sub-documents in bulk \n Loading… \n \n \n prefix \n __VIREO_SLOT_1__/**\n
\n \n __VIREO_SLOT_2__\n
\n\n \n \n The above action applies only to checked sub-documents. Leave as is These items will not change.\n
\n \n \n Save as automatic rule (Automatically applied to newly created documents under this prefix) \n \n \n\n
\n \n Related automatic rules \n \n __VIREO_SLOT_4__
\n \n
\n ",
+ "m_0c53e3964dd0d9de": "\n \n
\n \n Current document \n Apply to this document only \n \n __VIREO_SLOT_0__\n \n\n
\n \n Apply to sub-documents in bulk \n Loading… \n \n \n prefix \n __VIREO_SLOT_1__/**\n
\n \n __VIREO_SLOT_2__\n
\n\n \n \n The above action applies only to checked sub-documents. Leave as is These items will not change.\n
\n \n \n Save as automatic rule (Automatically applied to newly created documents under this prefix) \n \n \n\n
\n \n Related automatic rules \n \n __VIREO_SLOT_4__
\n \n
\n ",
"m_e549e8950801ce52": "'Apply ACL below' was selected but the flag is empty.",
"m_93b58fbaf5e77126": "No Changes",
"m_88daaeedfe4c69c6": "Saving…",
@@ -2335,7 +2335,7 @@
"m_5a85f5d54b42fdd3": "The prefix can be up to 200 characters long.",
"m_496b6b1ade3a4e6a": "Control characters cannot be used in the prefix.",
"m_1aee0ae203eabd47": "scanPrefixSubpages: disallowed column '__VIREO_SLOT_0__'",
- "m_6246136884f91732": "Categories can only contain Korean/English letters/numbers/spaces/commas.",
+ "m_6246136884f91732": "Categories can only contain Chinese/Korean/English letters/numbers/spaces/commas.",
"m_bc7101092992ca04": "Save auto category rule: __VIREO_SLOT_0__ → __VIREO_SLOT_1__",
"m_b9e3e99701625ec3": "Rule not found.",
"m_865de98deb7295d1": "Delete auto category rule: __VIREO_SLOT_0__",
diff --git a/packages/wiki-shared/src/i18n/source.json b/packages/wiki-shared/src/i18n/source.json
index 168e5fd..ce7be2f 100644
--- a/packages/wiki-shared/src/i18n/source.json
+++ b/packages/wiki-shared/src/i18n/source.json
@@ -628,7 +628,7 @@
"m_eba914ae07bfbadf": "SVG 보안 정제 라이브러리를 로드할 수 없습니다. 페이지를 새로고침 후 다시 시도해주세요.",
"m_16a254e99098adf0": "공백은 - 로 변환되고, 양 끝의 공백·- 는 제거됩니다. ",
"m_5d704cd6c4bfa10f": "< > ^ / \\ . ? 는 사용할 수 없습니다.",
- "m_a1f6b16b2c3b2a38": "\n 태그 (선택) \n \n \n
\n 한글/영문/숫자/공백/_/./- 만 사용 가능 · 최대 20개
",
+ "m_a1f6b16b2c3b2a38": "\n 태그 (선택) \n \n \n
\n 중국어/한글/영문/숫자/공백/_/./- 만 사용 가능 · 최대 20개
",
"m_ba18fae2953eebf1": "\n \n
파일명 (확장자 제외) \n
\n
__VIREO_SLOT_1__
\n __VIREO_SLOT_2__\n
\n ",
"m_0a1ac7eec7b927f7": "업로드",
"m_e6481000e178501b": "저장될 파일명: __VIREO_SLOT_0__",
@@ -1406,7 +1406,7 @@
"m_00bbefb0317222b2": "이 작업을 수행할 권한이 없습니다.",
"m_fcfc6676d4778b53": "이미지 编辑页面",
"m_a3030bf8f16dc63c": "保存",
- "m_cbadc087b51a5373": "\n \n
설명 \n
\n
태그 \n
\n \n
\n
한글/영문/숫자/공백/_/./- 만 사용 가능 · 최대 20개
\n
\n ",
+ "m_cbadc087b51a5373": "\n \n
설명 \n
\n
태그 \n
\n \n
\n
중국어/한글/영문/숫자/공백/_/./- 만 사용 가능 · 최대 20개
\n
\n ",
"m_193f229df3d99e3a": "已保存。",
"m_ac66748e68a838f4": "존재하지 않는 문서입니다",
"m_58d547bd560c8cb9": "\n \n 链入页面\n \n \n 문서 구조 보기\n \n ",
@@ -1878,7 +1878,7 @@
"m_41e7be2dde07e3b9": "\n \n 권한 \n \n __VIREO_SLOT_2__\n
\n 가입일 임계값(N일)은 관리자 콘솔 > 위키 설정의 편집 ACL 가입 일수 전역 설정을 따릅니다. 관리자 전용 플래그는 일반 사용자 편집을 일괄 차단합니다. \n \n ",
"m_8edcb1517374d55f": "\n \n \n \n 비공개 (관리자만 열람) \n \n
\n \n 편집 ACL \n 그대로 \n 비활성화 \n 아래 ACL 적용 \n
\n __VIREO_SLOT_3__\n \n \n \n 현재 문서에 저장\n \n
\n ",
"m_c5ac0714fceb948d": "문서 메타 로드 실패",
- "m_0c53e3964dd0d9de": "\n \n
\n\n
\n \n 하위 문서 일괄 적용 \n 불러오는 중… \n \n \n prefix \n __VIREO_SLOT_1__/**\n
\n \n __VIREO_SLOT_2__\n
\n\n \n \n 체크된 하위 문서에만 위 액션이 적용됩니다. 그대로 인 항목은 변경되지 않습니다.\n
\n \n \n 자동 규칙으로 저장 (이후 이 prefix 하위에 새로 만들어지는 문서에 자동 적용) \n \n \n\n
\n
\n ",
+ "m_0c53e3964dd0d9de": "\n \n
\n\n
\n \n 하위 문서 일괄 적용 \n 불러오는 중… \n \n \n prefix \n __VIREO_SLOT_1__/**\n
\n \n __VIREO_SLOT_2__\n
\n\n \n \n 체크된 하위 문서에만 위 액션이 적용됩니다. 그대로 인 항목은 변경되지 않습니다.\n
\n \n \n 자동 규칙으로 저장 (이후 이 prefix 하위에 새로 만들어지는 문서에 자동 적용) \n \n \n\n
\n
\n ",
"m_e549e8950801ce52": "'아래 ACL 적용' 을 선택했지만 플래그가 비어 있습니다.",
"m_93b58fbaf5e77126": "변경 사항이 없습니다.",
"m_88daaeedfe4c69c6": "저장 중…",
@@ -2335,7 +2335,7 @@
"m_5a85f5d54b42fdd3": "prefix 는 최대 200자까지 입력할 수 있습니다.",
"m_496b6b1ade3a4e6a": "prefix 에 제어문자를 사용할 수 없습니다.",
"m_1aee0ae203eabd47": "scanPrefixSubpages: disallowed column '__VIREO_SLOT_0__'",
- "m_6246136884f91732": "카테고리에는 한글/영문/숫자/공백/쉼표만 사용할 수 있습니다.",
+ "m_6246136884f91732": "카테고리에는 중국어/한글/영문/숫자/공백/쉼표만 사용할 수 있습니다.",
"m_bc7101092992ca04": "자동 카테고리 규칙 저장: __VIREO_SLOT_0__ → __VIREO_SLOT_1__",
"m_b9e3e99701625ec3": "규칙을 찾을 수 없습니다.",
"m_865de98deb7295d1": "자동 카테고리 규칙 삭제: __VIREO_SLOT_0__",
diff --git a/packages/wiki-shared/src/i18n/zh-CN.json b/packages/wiki-shared/src/i18n/zh-CN.json
index ebc37de..d404a73 100644
--- a/packages/wiki-shared/src/i18n/zh-CN.json
+++ b/packages/wiki-shared/src/i18n/zh-CN.json
@@ -628,7 +628,7 @@
"m_eba914ae07bfbadf": "无法加载 SVG 安全清理库。请刷新页面后再试。",
"m_16a254e99098adf0": "空格将 - 转换为,并且两端的空格·- 将被删除。 ",
"m_5d704cd6c4bfa10f": "< > ^ / \\ . ? 不可使用。",
- "m_a1f6b16b2c3b2a38": "\n 标签(可选) \n \n \n
\n 仅可使用韩文/英文/数字/空格/_/./- · 最多 20 个
",
+ "m_a1f6b16b2c3b2a38": "\n 标签(可选) \n \n \n
\n 仅可使用中文/韩文/英文/数字/空格/_/./- · 最多 20 个
",
"m_ba18fae2953eebf1": "\n \n
文件名(不含扩展名) \n
\n
__VIREO_SLOT_1__
\n __VIREO_SLOT_2__\n
\n ",
"m_0a1ac7eec7b927f7": "上传",
"m_e6481000e178501b": "保存的文件名: __VIREO_SLOT_0__",
@@ -1406,7 +1406,7 @@
"m_00bbefb0317222b2": "无权执行此操作。",
"m_fcfc6676d4778b53": "图片编辑页面",
"m_a3030bf8f16dc63c": "保存",
- "m_cbadc087b51a5373": "\n \n
说明 \n
\n
标签 \n
\n \n
\n
仅可使用韩文/英文/数字/空格/_/./- · 最多 20 个
\n
\n ",
+ "m_cbadc087b51a5373": "\n \n
说明 \n
\n
标签 \n
\n \n
\n
仅可使用中文/韩文/英文/数字/空格/_/./- · 最多 20 个
\n
\n ",
"m_193f229df3d99e3a": "已保存。",
"m_ac66748e68a838f4": "文档不存在",
"m_58d547bd560c8cb9": "\n \n 链入页面\n \n \n 查看文档结构\n \n ",
@@ -1878,7 +1878,7 @@
"m_41e7be2dde07e3b9": "\n \n 权限 \n \n __VIREO_SLOT_2__\n
\n 注册日期阈值(N天)在管理员控制台>维基设置中 编辑ACL注册天数 遵循全局设置。 管理员专用 标记将批量阻止普通用户编辑。 \n \n ",
"m_8edcb1517374d55f": "\n \n \n \n 私有(仅管理员可查看) \n \n
\n \n 编辑ACL \n 保持原样 \n 禁用 \n 应用于下列ACL \n
\n __VIREO_SLOT_3__\n \n \n \n 保存到当前文档\n \n
\n ",
"m_c5ac0714fceb948d": "文档元加载失败",
- "m_0c53e3964dd0d9de": "\n \n
\n\n
\n \n \n 前缀 \n __VIREO_SLOT_1__/**\n
\n \n __VIREO_SLOT_2__\n
\n\n \n \n 仅对选中的子文档应用上述操作。 保持原样 这些项目不会更改。\n
\n \n \n 以自动规则保存 (将自动应用于之后在此前缀下新创建的文档) \n \n \n\n
\n
\n ",
+ "m_0c53e3964dd0d9de": "\n \n
\n\n
\n \n \n 前缀 \n __VIREO_SLOT_1__/**\n
\n \n __VIREO_SLOT_2__\n
\n\n \n \n 仅对选中的子文档应用上述操作。 保持原样 这些项目不会更改。\n
\n \n \n 以自动规则保存 (将自动应用于之后在此前缀下新创建的文档) \n \n \n\n
\n
\n ",
"m_e549e8950801ce52": "选择了“应用下列ACL”,但标记为空。",
"m_93b58fbaf5e77126": "无更改",
"m_88daaeedfe4c69c6": "正在保存…",
@@ -2335,7 +2335,7 @@
"m_5a85f5d54b42fdd3": "前缀最多可输入 200 个字符。",
"m_496b6b1ade3a4e6a": "前缀不能使用控制字符。",
"m_1aee0ae203eabd47": "scanPrefixSubpages:不允许的列 '__VIREO_SLOT_0__'",
- "m_6246136884f91732": "类别只能使用韩文/英文/数字/空格/逗号。",
+ "m_6246136884f91732": "类别只能使用中文/韩文/英文/数字/空格/逗号。",
"m_bc7101092992ca04": "自动分类规则保存: __VIREO_SLOT_0__ → __VIREO_SLOT_1__",
"m_b9e3e99701625ec3": "找不到规则。",
"m_865de98deb7295d1": "自动分类规则删除: __VIREO_SLOT_0__",
diff --git a/src/client/common.ts b/src/client/common.ts
index 5e37edf..9fc5b74 100644
--- a/src/client/common.ts
+++ b/src/client/common.ts
@@ -100,7 +100,7 @@ function escapeHtml(str) {
// ── 미디어 태그 입력 위젯 (카테고리 입력과 동일한 UX) ──
// 컨테이너에 버블 UI + 자동완성(/api/media/search-tags)을 장착한다.
// 업로드 모달, 이미지 검색 모달, 이미지 문서 편집 모달에서 공통 사용한다.
-const MEDIA_TAG_VALID_RE = /^[가-힣a-zA-Z0-9 _.-]+$/;
+const MEDIA_TAG_VALID_RE = /^[\p{Script=Han}가-힣a-zA-Z0-9 _.-]+$/u;
function mountMediaTagInput({ container, input, initial }) {
const tags = Array.isArray(initial) ? initial.slice() : [];
diff --git a/src/client/edit/autocomplete.ts b/src/client/edit/autocomplete.ts
index 409b53c..a3532e5 100644
--- a/src/client/edit/autocomplete.ts
+++ b/src/client/edit/autocomplete.ts
@@ -1580,7 +1580,7 @@ async function addCategoryTag(tag: string): Promise {
if (!window.categoryTags) window.categoryTags = [];
if (!window.categoryAclChoices) window.categoryAclChoices = {};
- if (!/^[가-힣a-zA-Z0-9\s_.-]+$/.test(cleanTag)) {
+ if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s_.-]+$/u.test(cleanTag)) {
window.Swal?.fire({
icon: 'warning',
title: ui("m_63d43c7f9b539745"),
diff --git a/src/client/edit/bulk-category.ts b/src/client/edit/bulk-category.ts
index 1cfe133..3e48cbd 100644
--- a/src/client/edit/bulk-category.ts
+++ b/src/client/edit/bulk-category.ts
@@ -150,7 +150,7 @@ function hookRuleDeleteButtons(container: HTMLElement, relatedTo: string) {
// 카테고리 칩 한 항목의 형식 검증 — 백엔드 CATEGORY_PATTERN 이 콤마 포함 문자열
// 전체에 한글/영문/숫자/공백/쉼표만 허용하므로, 칩 단위로는 콤마를 제외한
// 한글/영문/숫자/공백만 허용한다.
-const BULK_CAT_TAG_RE = /^[가-힣a-zA-Z0-9\s]+$/;
+const BULK_CAT_TAG_RE = /^[\p{Script=Han}가-힣a-zA-Z0-9\s]+$/u;
interface BulkCatAcState {
visible: boolean;
diff --git a/src/client/edit/main.ts b/src/client/edit/main.ts
index 1802919..1cce8da 100644
--- a/src/client/edit/main.ts
+++ b/src/client/edit/main.ts
@@ -2871,7 +2871,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const _autoSlugCat = (() => {
if (!slug?.startsWith('카테고리:')) return null;
const n = slug.slice('카테고리:'.length).trim();
- return (n && /^[가-힣a-zA-Z0-9\s]+$/.test(n)) ? n : null;
+ return (n && /^[\p{Script=Han}가-힣a-zA-Z0-9\s]+$/u.test(n)) ? n : null;
})();
if (_autoSlugCat && !categoryTags.includes(_autoSlugCat)) {
categoryTags.unshift(_autoSlugCat);
@@ -3041,7 +3041,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// 카테고리 문서(카테고리:이름)는 신규 생성 시에도 해당 카테고리를 미리 주입
if (slug?.startsWith('카테고리:')) {
const _autoNewCat = slug.slice('카테고리:'.length).trim();
- if (_autoNewCat && /^[가-힣a-zA-Z0-9\s]+$/.test(_autoNewCat) && !categoryTags.includes(_autoNewCat)) {
+ if (_autoNewCat && /^[\p{Script=Han}가-힣a-zA-Z0-9\s]+$/u.test(_autoNewCat) && !categoryTags.includes(_autoNewCat)) {
categoryTags.unshift(_autoNewCat);
document.getElementById('categoryInput').value = categoryTags.join(',');
syncStateToWindow();
@@ -3954,7 +3954,7 @@ async function savePage() {
return;
}
- if (category && !/^[가-힣a-zA-Z0-9\s,]+$/.test(category)) {
+ if (category && !/^[\p{Script=Han}가-힣a-zA-Z0-9\s,]+$/u.test(category)) {
window.Swal.fire(ui("m_0bc1fb72ae1be5c5"), ui("m_a21df18dbb6dce0c"), 'warning');
return;
}
diff --git a/src/client/permissions/permissions-modal.ts b/src/client/permissions/permissions-modal.ts
index 6f08814..d5f0855 100644
--- a/src/client/permissions/permissions-modal.ts
+++ b/src/client/permissions/permissions-modal.ts
@@ -696,7 +696,7 @@ export async function openPermissionsModal(rawSlug: string): Promise {
const addTag = (tag: string) => {
const cleanTag = tag.trim();
if (!cleanTag) return;
- if (!/^[가-힣a-zA-Z0-9\s_.-]+$/.test(cleanTag)) {
+ if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s_.-]+$/u.test(cleanTag)) {
swal.fire({
icon: 'warning',
title: ui("m_4907df8dbe60e7cc"),
@@ -791,7 +791,7 @@ export async function openPermissionsModal(rawSlug: string): Promise {
}
if (categoriesAction === 'add' || categoriesAction === 'set') {
const raw = readBulkCatInput();
- if (!/^[가-힣a-zA-Z0-9\s_.,-]+$/.test(raw)) {
+ if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s_.,-]+$/u.test(raw)) {
swal.showValidationMessage(ui("m_35926634b2949979"));
return false;
}
diff --git a/src/routes/admin-mcp.ts b/src/routes/admin-mcp.ts
index 4f1e42c..b960460 100644
--- a/src/routes/admin-mcp.ts
+++ b/src/routes/admin-mcp.ts
@@ -1022,7 +1022,7 @@ export async function dispatchAdminEditTool(c: Context, user: User, toolNam
if (slug.startsWith('이미지:')) return asTextResult(ui("m_e02821f6bffe9ca0"), true);
if (slug.startsWith('map:')) return asTextResult(ui("m_51f6566a2dc68dac"), true); if (typeof args.content !== 'string') return asTextResult(ui("m_b3104adc425b92b3"), true);
if (args.category && typeof args.category === 'string') {
- if (!/^[가-힣a-zA-Z0-9\s,]+$/.test(args.category)) {
+ if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s,]+$/u.test(args.category)) {
return asTextResult(ui("m_ecc6b4ed43d3fa11"), true);
}
}
@@ -2178,7 +2178,7 @@ export async function dispatchAdminEditTool(c: Context, user: User, toolNam
const trimmedCategory = (args.category as string).trim();
const newCategory = trimmedCategory ? trimmedCategory : null;
- if (newCategory && !/^[가-힣a-zA-Z0-9\s,]+$/.test(newCategory)) {
+ if (newCategory && !/^[\p{Script=Han}가-힣a-zA-Z0-9\s,]+$/u.test(newCategory)) {
return asTextResult(ui("m_ecc6b4ed43d3fa11"), true);
}
diff --git a/src/routes/admin.ts b/src/routes/admin.ts
index 66ebf00..b1300a2 100644
--- a/src/routes/admin.ts
+++ b/src/routes/admin.ts
@@ -1198,7 +1198,7 @@ adminRoutes.put('/category-acl/:name', async (c) => {
const currentUser = c.get('user')!;
const name = c.req.param('name');
if (!name) return c.json({ error: ui("m_fd04e6fd149684a1") }, 400);
- if (!/^[가-힣a-zA-Z0-9\s_.-]+$/.test(name)) {
+ if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s_.-]+$/u.test(name)) {
return c.json({ error: ui("m_592c3fe662fb1c68") }, 400);
}
@@ -1304,7 +1304,7 @@ adminRoutes.post('/category-acl/:name/bulk-apply', async (c) => {
const currentUser = c.get('user')!;
const name = c.req.param('name');
if (!name) return c.json({ error: ui("m_fd04e6fd149684a1") }, 400);
- if (!/^[가-힣a-zA-Z0-9\s_.-]+$/.test(name)) {
+ if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s_.-]+$/u.test(name)) {
return c.json({ error: ui("m_592c3fe662fb1c68") }, 400);
}
@@ -2095,7 +2095,7 @@ adminRoutes.get('/pages/deleted', async (c) => {
// 관리자 전용 (이 라우터 자체가 requireAdmin 미들웨어로 보호됨).
const PREFIX_FORBIDDEN_CHARS = /[\x00-\x1F\x7F]/;
-const CATEGORY_PATTERN = /^[가-힣a-zA-Z0-9\s,]+$/;
+const CATEGORY_PATTERN = /^[\p{Script=Han}가-힣a-zA-Z0-9\s,]+$/u;
function normalizeCategoryString(s: string): string {
return s
diff --git a/src/routes/wiki.ts b/src/routes/wiki.ts
index b626fee..daab07a 100644
--- a/src/routes/wiki.ts
+++ b/src/routes/wiki.ts
@@ -329,7 +329,7 @@ export function getCategoryDocAutoCategory(slug: string): string | null {
if (!slug.startsWith(prefix)) return null;
const name = slug.slice(prefix.length).trim();
if (!name) return null;
- if (!/^[가-힣a-zA-Z0-9\s]+$/.test(name)) return null;
+ if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s]+$/u.test(name)) return null;
return name;
}
@@ -1094,7 +1094,7 @@ wiki.post('/w/check-category', requireAuth, async (c) => {
if (!raw) {
return c.json({ ok: false, reason: 'invalid' as const });
}
- if (!/^[가-힣a-zA-Z0-9\s_.-]+$/.test(raw)) {
+ if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s_.-]+$/u.test(raw)) {
return c.json({ ok: false, reason: 'invalid' as const });
}
@@ -2189,7 +2189,7 @@ wiki.put('/w/:slug', requireAuth, async (c) => {
// 보안: 카테고리 특수문자 금지 (한글, 영문, 숫자, 공백, 쉼표만 허용)
if (body.category) {
- const categoryPattern = /^[가-힣a-zA-Z0-9\s,]+$/;
+ const categoryPattern = /^[\p{Script=Han}가-힣a-zA-Z0-9\s,]+$/u;
if (!categoryPattern.test(body.category)) {
return c.json({ error: ui("m_a21df18dbb6dce0c") }, 400);
}
diff --git a/src/utils/mediaTags.ts b/src/utils/mediaTags.ts
index 7ea9ce2..f5d5f2a 100644
--- a/src/utils/mediaTags.ts
+++ b/src/utils/mediaTags.ts
@@ -5,11 +5,11 @@
*/
/**
- * 태그 문자열 규칙: 카테고리와 동일 (한글/영숫자/공백/_/./-).
+ * 태그 문자열 규칙: 중국어/한글/영숫자/공백/_/./-.
* 최대 20개, 각 50자 이내. trim, 중복 제거, 정규식 통과 항목만 유효.
* 입력은 배열 / JSON 배열 문자열 / 쉼표구분 문자열을 모두 허용한다.
*/
-const TAG_VALID_RE = /^[가-힣a-zA-Z0-9 _.-]+$/;
+const TAG_VALID_RE = /^[\p{Script=Han}가-힣a-zA-Z0-9 _.-]+$/u;
const TAG_MAX_COUNT = 20;
const TAG_MAX_LENGTH = 50;