1
0
forked from baron/baron-sso

worksmobile 연동 & ory stack 26.2.0으로 업그레이드

This commit is contained in:
2026-05-06 09:30:00 +09:00
parent 3dcdd97882
commit 2495fcb13d
74 changed files with 8698 additions and 212 deletions

View File

@@ -44,6 +44,13 @@ import {
} from "../../components/ui/dialog";
import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import { Switch } from "../../components/ui/switch";
import {
Tabs,
@@ -78,6 +85,7 @@ import {
parseOrgChartTenantSelection,
} from "./orgChartPicker";
import type { UserSchemaField } from "./userSchemaFields";
import { userStatusLabel, userStatusValues } from "./userStatus";
type UserFormValues = Omit<UserUpdateRequest, "metadata"> & {
metadata: Record<string, Record<string, string | number | boolean>>;
@@ -123,12 +131,40 @@ function createEmptyAppointment(): AppointmentDraft {
tenantId: "",
tenantName: "",
tenantSlug: "",
isPrimary: false,
isOwner: false,
jobTitle: "",
position: "",
};
}
function normalizePrimaryAppointments(
appointments: AppointmentDraft[],
): AppointmentDraft[] {
const leafIndexes = appointments
.map((appointment, index) =>
appointment.tenantId.trim().length > 0 ? index : -1,
)
.filter((index) => index >= 0);
if (leafIndexes.length === 1) {
const primaryIndex = leafIndexes[0];
return appointments.map((appointment, index) => ({
...appointment,
isPrimary: index === primaryIndex,
}));
}
const selectedIndex = appointments.findIndex(
(appointment) => appointment.isPrimary === true,
);
return appointments.map((appointment, index) => ({
...appointment,
isPrimary:
selectedIndex >= 0 &&
index === selectedIndex &&
appointment.tenantId.trim().length > 0,
}));
}
function validateManualPassword(
password: string,
policy?: PasswordPolicyResponse,
@@ -485,15 +521,17 @@ function UserDetailPage() {
try {
const tenant = await resolveTenantSelection(selection, tenants);
setAdditionalAppointments((current) =>
current.map((appointment, index) =>
index === target.index
? {
...appointment,
tenantId: tenant.id,
tenantName: tenant.name,
tenantSlug: tenant.slug,
}
: appointment,
normalizePrimaryAppointments(
current.map((appointment, index) =>
index === target.index
? {
...appointment,
tenantId: tenant.id,
tenantName: tenant.name,
tenantSlug: tenant.slug,
}
: appointment,
),
),
);
setPickerTarget(null);
@@ -536,15 +574,30 @@ function UserDetailPage() {
patch: Partial<UserAppointment>,
) => {
setAdditionalAppointments((current) =>
current.map((appointment, currentIndex) =>
currentIndex === index ? { ...appointment, ...patch } : appointment,
normalizePrimaryAppointments(
current.map((appointment, currentIndex) =>
currentIndex === index ? { ...appointment, ...patch } : appointment,
),
),
);
};
const setPrimaryAppointment = (index: number, checked: boolean) => {
setAdditionalAppointments((current) =>
normalizePrimaryAppointments(
current.map((appointment, currentIndex) => ({
...appointment,
isPrimary: checked && currentIndex === index,
})),
),
);
};
const removeAppointment = (index: number) => {
setAdditionalAppointments((current) =>
current.filter((_, currentIndex) => currentIndex !== index),
normalizePrimaryAppointments(
current.filter((_, currentIndex) => currentIndex !== index),
),
);
};
@@ -602,7 +655,10 @@ function UserDetailPage() {
tenantSlug:
user.companyCode ||
user.joinedTenants?.find(
(t) => t.type === "COMPANY" || t.type === "COMPANY_GROUP",
(t) =>
t.type === "COMPANY" ||
t.type === "COMPANY_GROUP" ||
t.type === "ORGANIZATION",
)?.slug ||
"",
department: user.department || "",
@@ -636,38 +692,45 @@ function UserDetailPage() {
isHanmacFamilyTenant(tenant, tenants, hanmacFamilyTenantId),
);
setAdditionalAppointments(
Array.isArray(rawAppointments)
? (rawAppointments as UserAppointment[]).map((appointment) => ({
...appointment,
draftId: createDraftId(),
}))
: isUserHanmacFamily
? familyFallbackTenants.length > 0
? familyFallbackTenants.map((tenant) => ({
draftId: createDraftId(),
tenantId: tenant.id,
tenantName: tenant.name,
tenantSlug: tenant.slug,
isOwner:
metadata.primaryTenantIsOwner === true &&
tenant.id === fallbackAppointment?.id,
jobTitle: user.jobTitle,
position: user.position,
}))
: fallbackAppointment
? [
{
draftId: createDraftId(),
tenantId: fallbackAppointment.id,
tenantName: fallbackAppointment.name,
tenantSlug: fallbackAppointment.slug,
isOwner: metadata.primaryTenantIsOwner === true,
jobTitle: user.jobTitle,
position: user.position,
},
]
: []
: [],
normalizePrimaryAppointments(
Array.isArray(rawAppointments)
? (rawAppointments as UserAppointment[]).map((appointment) => ({
...appointment,
isPrimary:
appointment.isPrimary === true ||
appointment.tenantId === metadata.primaryTenantId,
draftId: createDraftId(),
}))
: isUserHanmacFamily
? familyFallbackTenants.length > 0
? familyFallbackTenants.map((tenant) => ({
draftId: createDraftId(),
tenantId: tenant.id,
tenantName: tenant.name,
tenantSlug: tenant.slug,
isPrimary: tenant.id === fallbackAppointment?.id,
isOwner:
metadata.primaryTenantIsOwner === true &&
tenant.id === fallbackAppointment?.id,
jobTitle: user.jobTitle,
position: user.position,
}))
: fallbackAppointment
? [
{
draftId: createDraftId(),
tenantId: fallbackAppointment.id,
tenantName: fallbackAppointment.name,
tenantSlug: fallbackAppointment.slug,
isPrimary: true,
isOwner: metadata.primaryTenantIsOwner === true,
jobTitle: user.jobTitle,
position: user.position,
},
]
: []
: [],
),
);
}
}, [hanmacFamilyTenantId, tenants, user, reset]);
@@ -748,19 +811,37 @@ function UserDetailPage() {
tenantId: appointment.tenantId,
tenantSlug: appointment.tenantSlug,
tenantName: appointment.tenantName,
isPrimary: appointment.isPrimary === true,
isOwner: appointment.isOwner,
jobTitle: appointment.jobTitle,
position: appointment.position,
}));
const primaryAppointment = appointments.find(
(appointment) => appointment.isPrimary,
);
payload.tenantSlug = undefined;
payload.department = undefined;
payload.position = undefined;
payload.jobTitle = undefined;
payload.additionalAppointments = appointments;
if (primaryAppointment) {
payload.tenantSlug = primaryAppointment.tenantSlug;
payload.primaryTenantId = primaryAppointment.tenantId;
payload.primaryTenantName = primaryAppointment.tenantName;
payload.primaryTenantIsOwner = primaryAppointment.isOwner;
}
payload.metadata = {
...metadata,
additionalAppointments: appointments,
...(primaryAppointment
? {
primaryTenantId: primaryAppointment.tenantId,
primaryTenantName: primaryAppointment.tenantName,
primaryTenantSlug: primaryAppointment.tenantSlug,
primaryTenantIsOwner: primaryAppointment.isOwner,
}
: {}),
};
}
@@ -791,6 +872,9 @@ function UserDetailPage() {
filterNonHanmacFamilyTenants(userAffiliatedTenants, hanmacFamilyTenantId),
[userAffiliatedTenants, hanmacFamilyTenantId],
);
const primaryAppointmentLeafCount = additionalAppointments.filter(
(appointment) => appointment.tenantId.trim().length > 0,
).length;
if (isLoading) {
return (
@@ -857,7 +941,10 @@ function UserDetailPage() {
{user.tenant?.name ||
user.companyCode ||
user.joinedTenants?.find(
(t) => t.type === "COMPANY" || t.type === "COMPANY_GROUP",
(t) =>
t.type === "COMPANY" ||
t.type === "COMPANY_GROUP" ||
t.type === "ORGANIZATION",
)?.name ||
t("ui.admin.users.detail.form.tenant_global", "시스템 전역")}
</Badge>
@@ -1001,21 +1088,23 @@ function UserDetailPage() {
>
{t("ui.admin.users.detail.form.status", "상태")}
</Label>
<div className="flex h-11 items-center gap-3 rounded-md border border-input bg-background px-3">
<Switch
id="status"
checked={watchedStatus === "active"}
onCheckedChange={(checked) =>
setValue("status", checked ? "active" : "inactive")
}
/>
<span className="text-sm text-muted-foreground">
{t(
`ui.common.status.${watchedStatus}`,
watchedStatus || "inactive",
)}
</span>
</div>
<Select
value={watchedStatus || "inactive"}
onValueChange={(status) => setValue("status", status)}
>
<SelectTrigger id="status" className="h-11">
<SelectValue>
{userStatusLabel(watchedStatus || "inactive")}
</SelectValue>
</SelectTrigger>
<SelectContent>
{userStatusValues.map((status) => (
<SelectItem key={status} value={status}>
{userStatusLabel(status)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
@@ -1160,6 +1249,26 @@ function UserDetailPage() {
{appointment.tenantSlug}
</span>
)}
<label className="flex items-center gap-3 text-sm">
<Switch
aria-label={t(
"ui.admin.users.detail.form.appointment_primary",
"대표 조직",
)}
checked={appointment.isPrimary === true}
disabled={
!appointment.tenantId ||
primaryAppointmentLeafCount <= 1
}
onCheckedChange={(checked) =>
setPrimaryAppointment(index, checked)
}
/>
{t(
"ui.admin.users.detail.form.appointment_primary",
"대표 조직",
)}
</label>
<label className="flex items-center gap-3 text-sm">
<Checkbox
checked={appointment.isOwner}