From db88c7ab1cbfbe8e7c1a7a0e5dc8926ba033cb68 Mon Sep 17 00:00:00 2001 From: chan Date: Wed, 4 Mar 2026 10:09:52 +0900 Subject: [PATCH] feat: implement enhanced user schema management with validation and admin_only fields --- .../tenants/routes/TenantSchemaPage.tsx | 221 ++++++++++++------ .../src/features/users/UserCreatePage.tsx | 66 +++++- .../src/features/users/UserDetailPage.tsx | 57 ++++- backend/internal/handler/user_handler.go | 98 ++++++++ 4 files changed, 346 insertions(+), 96 deletions(-) diff --git a/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx b/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx index d9032257..bf4b2315 100644 --- a/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx @@ -17,7 +17,7 @@ import { Label } from "../../../components/ui/label"; import { fetchTenant, updateTenant } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; -type SchemaFieldType = "text" | "number" | "boolean"; +type SchemaFieldType = "text" | "number" | "boolean" | "date"; type SchemaField = { id: string; @@ -25,6 +25,8 @@ type SchemaField = { label: string; type: SchemaFieldType; required: boolean; + adminOnly: boolean; + validation?: string; }; function createFieldId() { @@ -62,10 +64,14 @@ export function TenantSchemaPage() { key: typeof field?.key === "string" ? field.key : "", label: typeof field?.label === "string" ? field.label : "", type: - field?.type === "number" || field?.type === "boolean" + field?.type === "number" || + field?.type === "boolean" || + field?.type === "date" ? field.type : "text", required: Boolean(field?.required), + adminOnly: Boolean(field?.adminOnly), + validation: typeof field?.validation === "string" ? field.validation : "", })), ); } @@ -105,6 +111,8 @@ export function TenantSchemaPage() { label: "", type: "text", required: false, + adminOnly: false, + validation: "", }, ]); }; @@ -141,7 +149,7 @@ export function TenantSchemaPage() { - + {fields.length === 0 && (
{t( @@ -153,84 +161,142 @@ export function TenantSchemaPage() { {fields.map((field, index) => (
-
- - updateField(index, { key: e.target.value })} - placeholder={t( - "ui.admin.tenants.schema.field.key_placeholder", - "예: employee_id", - )} - className="h-10" - /> -
-
- - - updateField(index, { label: e.target.value }) - } - placeholder={t( - "ui.admin.tenants.schema.field.label_placeholder", - "예: 사번", - )} - className="h-10" - /> -
-
- - updateField(index, { key: e.target.value })} + placeholder={t( + "ui.admin.tenants.schema.field.key_placeholder", + "예: employee_id", + )} + className="h-10" + /> +
+
+ + + updateField(index, { label: e.target.value }) } - }} - > - - - - + className="h-10" + /> +
+
+ + +
+
+ +
+
+ + +
+
+ + updateField(index, { validation: e.target.value }) + } + placeholder={t( + "ui.admin.tenants.schema.field.validation_placeholder", + "정규식 (예: ^[0-9]+$)", + )} + className="h-9 text-xs font-mono" + /> +
+
+ +
-
))}
@@ -249,3 +315,4 @@ export function TenantSchemaPage() { ); } + diff --git a/adminfront/src/features/users/UserCreatePage.tsx b/adminfront/src/features/users/UserCreatePage.tsx index 5e506677..3147c701 100644 --- a/adminfront/src/features/users/UserCreatePage.tsx +++ b/adminfront/src/features/users/UserCreatePage.tsx @@ -26,8 +26,10 @@ import { t } from "../../lib/i18n"; type UserSchemaField = { key: string; label?: string; - type?: "text" | "number" | "boolean"; + type?: "text" | "number" | "boolean" | "date"; required?: boolean; + adminOnly?: boolean; + validation?: string; }; type UserFormValues = UserCreateRequest & { metadata: Record }; @@ -85,8 +87,24 @@ function UserCreatePage() { ? (tenantDetail?.config?.userSchema as UserSchemaField[]) : []; - const registerMetadata = (key: string) => - register(`metadata.${key}` as `metadata.${string}`); + const registerMetadata = (field: UserSchemaField) => + register(`metadata.${field.key}` as `metadata.${string}`, { + required: field.required + ? t("msg.admin.users.create.form.field_required", "{{label}}은(는) 필수입니다.", { + label: field.label || field.key, + }) + : false, + pattern: field.validation + ? { + value: new RegExp(field.validation), + message: t( + "msg.admin.users.create.form.field_invalid", + "{{label}} 형식이 올바르지 않습니다.", + { label: field.label || field.key }, + ), + } + : undefined, + }); const mutation = useMutation({ mutationFn: createUser, @@ -107,17 +125,16 @@ function UserCreatePage() { }, }); - const onSubmit = (data: UserCreateRequest) => { + const onSubmit = (data: UserFormValues) => { setError(null); setGeneratedPassword(null); setCreatedEmail(null); - if (autoPassword) { - mutation.mutate({ ...data, password: "" }); - return; - } + const payload = { ...data }; - if (!data.password) { + if (autoPassword) { + payload.password = ""; + } else if (!data.password) { setError( t( "msg.admin.users.create.password_required", @@ -127,7 +144,7 @@ function UserCreatePage() { return; } - mutation.mutate(data); + mutation.mutate(payload); }; const onCopyPassword = async () => { @@ -414,13 +431,37 @@ function UserCreatePage() {
+ {errors.metadata?.[field.key] && ( +

+ {(errors.metadata[field.key] as any).message} +

+ )}
))} @@ -482,4 +523,5 @@ function UserCreatePage() { ); } + export default UserCreatePage; diff --git a/adminfront/src/features/users/UserDetailPage.tsx b/adminfront/src/features/users/UserDetailPage.tsx index f4e98948..e85d5d87 100644 --- a/adminfront/src/features/users/UserDetailPage.tsx +++ b/adminfront/src/features/users/UserDetailPage.tsx @@ -26,8 +26,10 @@ import { t } from "../../lib/i18n"; type UserSchemaField = { key: string; label?: string; - type?: "text" | "number" | "boolean"; + type?: "text" | "number" | "boolean" | "date"; required?: boolean; + adminOnly?: boolean; + validation?: string; }; type UserFormValues = UserUpdateRequest & { metadata: Record }; @@ -94,8 +96,24 @@ function UserDetailPage() { ? (tenantDetail?.config?.userSchema as UserSchemaField[]) : []; - const registerMetadata = (key: string) => - register(`metadata.${key}` as `metadata.${string}`); + const registerMetadata = (field: UserSchemaField) => + register(`metadata.${field.key}` as `metadata.${string}`, { + required: field.required + ? t("msg.admin.users.detail.form.field_required", "{{label}}은(는) 필수입니다.", { + label: field.label || field.key, + }) + : false, + pattern: field.validation + ? { + value: new RegExp(field.validation), + message: t( + "msg.admin.users.detail.form.field_invalid", + "{{label}} 형식이 올바르지 않습니다.", + { label: field.label || field.key }, + ), + } + : undefined, + }); React.useEffect(() => { if (user) { @@ -139,8 +157,8 @@ function UserDetailPage() { }, }); - const onSubmit = (data: UserUpdateRequest) => { - const payload = { ...data }; + const onSubmit = (data: UserFormValues) => { + const payload: UserUpdateRequest = { ...data }; if (!payload.password) { payload.password = undefined; } @@ -393,13 +411,37 @@ function UserDetailPage() {
+ {errors.metadata?.[field.key] && ( +

+ {(errors.metadata[field.key] as any).message} +

+ )}
))} @@ -410,6 +452,7 @@ function UserDetailPage() {

{t("ui.admin.users.detail.security.title", "보안 설정")}

+