1
0
forked from baron/baron-sso

조직현황 구조변경. 총괄센터삼안 실 조직 삽입확인

This commit is contained in:
2026-05-11 20:13:54 +09:00
parent d3853fac2a
commit 3063450ee0
59 changed files with 5086 additions and 549 deletions

View File

@@ -98,6 +98,7 @@ function createEmptyAppointment(): AppointmentDraft {
tenantName: "",
tenantSlug: "",
isOwner: false,
grade: "",
jobTitle: "",
position: "",
};
@@ -148,6 +149,7 @@ function UserCreatePage() {
phone: "",
tenantSlug: searchParams.get("tenantSlug") || "",
department: "",
grade: "",
position: "",
jobTitle: "",
metadata: {},
@@ -379,6 +381,7 @@ function UserCreatePage() {
}
payload.tenantSlug = data.tenantSlug;
payload.department = data.department;
payload.grade = data.grade;
payload.position = data.position;
payload.jobTitle = data.jobTitle;
}
@@ -411,6 +414,7 @@ function UserCreatePage() {
tenantName: appointment.tenantName,
isPrimary: appointment.isOwner,
isOwner: appointment.isOwner,
grade: appointment.grade,
jobTitle: appointment.jobTitle,
position: appointment.position,
}));
@@ -685,12 +689,20 @@ function UserCreatePage() {
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-4 md:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="position"></Label>
<Label htmlFor="grade"></Label>
<Input
id="grade"
placeholder="수석/책임/선임"
{...register("grade")}
/>
</div>
<div className="space-y-2">
<Label htmlFor="position"></Label>
<Input
id="position"
placeholder="수석/책임/선임"
placeholder="팀장/센터장"
{...register("position")}
/>
</div>
@@ -709,9 +721,11 @@ function UserCreatePage() {
<div className="space-y-3">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium"> /</p>
<p className="text-sm font-medium">
//
</p>
<p className="text-xs text-muted-foreground">
, , .
, , , .
</p>
</div>
<Button
@@ -778,9 +792,23 @@ function UserCreatePage() {
</div>
<div
className="grid gap-3 sm:grid-cols-2"
className="grid gap-3 sm:grid-cols-3"
data-testid={`appointment-position-line-${index}`}
>
<div className="space-y-2">
<Label htmlFor={`appointment-grade-${index}`}>
</Label>
<Input
id={`appointment-grade-${index}`}
value={appointment.grade ?? ""}
onChange={(event) =>
updateAppointment(index, {
grade: event.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label htmlFor={`appointment-job-title-${index}`}>
@@ -797,7 +825,7 @@ function UserCreatePage() {
</div>
<div className="space-y-2">
<Label htmlFor={`appointment-position-${index}`}>
</Label>
<Input
id={`appointment-position-${index}`}

View File

@@ -125,6 +125,7 @@ function createEmptyAppointment(): AppointmentDraft {
tenantSlug: "",
isPrimary: false,
isOwner: false,
grade: "",
jobTitle: "",
position: "",
};
@@ -379,6 +380,7 @@ function UserDetailPage() {
status: "active",
tenantSlug: "",
department: "",
grade: "",
position: "",
jobTitle: "",
metadata: {},
@@ -622,6 +624,7 @@ function UserDetailPage() {
)?.slug ||
"",
department: user.department || "",
grade: user.grade || "",
position: user.position || "",
jobTitle: user.jobTitle || "",
metadata:
@@ -671,6 +674,7 @@ function UserDetailPage() {
isOwner:
metadata.primaryTenantIsOwner === true &&
tenant.id === fallbackAppointment?.id,
grade: user.grade,
jobTitle: user.jobTitle,
position: user.position,
}))
@@ -683,6 +687,7 @@ function UserDetailPage() {
tenantSlug: fallbackAppointment.slug,
isPrimary: true,
isOwner: metadata.primaryTenantIsOwner === true,
grade: user.grade,
jobTitle: user.jobTitle,
position: user.position,
},
@@ -750,6 +755,7 @@ function UserDetailPage() {
const tenant = await ensurePersonalTenant();
payload.tenantSlug = tenant.slug;
payload.department = undefined;
payload.grade = undefined;
payload.position = undefined;
payload.jobTitle = undefined;
payload.metadata = {
@@ -771,6 +777,7 @@ function UserDetailPage() {
tenantName: appointment.tenantName,
isPrimary: appointment.isOwner,
isOwner: appointment.isOwner,
grade: appointment.grade,
jobTitle: appointment.jobTitle,
position: appointment.position,
}));
@@ -790,6 +797,7 @@ function UserDetailPage() {
}
payload.department = undefined;
payload.grade = undefined;
payload.position = undefined;
payload.jobTitle = undefined;
payload.additionalAppointments = appointments;
@@ -1142,13 +1150,13 @@ function UserDetailPage() {
<p className="text-sm font-medium">
{t(
"ui.admin.users.detail.form.additional_appointments",
"소속별 직급/직무",
"소속별 직급/직책/직무",
)}
</p>
<p className="text-xs text-muted-foreground">
{t(
"msg.admin.users.detail.form.additional_appointments_help",
"테넌트별 조직장 여부, 직, 직급을 입력합니다.",
"테넌트별 조직장 여부, 직, 직책, 직무를 입력합니다.",
)}
</p>
</div>
@@ -1226,9 +1234,28 @@ function UserDetailPage() {
</div>
<div
className="grid gap-3 sm:grid-cols-2"
className="grid gap-3 sm:grid-cols-3"
data-testid={`detail-appointment-position-line-${index}`}
>
<div className="space-y-2">
<Label
htmlFor={`detail-appointment-grade-${index}`}
>
{t(
"ui.admin.users.detail.form.grade",
"직급",
)}
</Label>
<Input
id={`detail-appointment-grade-${index}`}
value={appointment.grade ?? ""}
onChange={(event) =>
updateAppointment(index, {
grade: event.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label
htmlFor={`detail-appointment-job-title-${index}`}
@@ -1255,7 +1282,7 @@ function UserDetailPage() {
>
{t(
"ui.admin.users.detail.form.position",
"직",
"직",
)}
</Label>
<Input
@@ -1313,12 +1340,25 @@ function UserDetailPage() {
className="h-11 shadow-sm"
/>
</div>
<div className="space-y-2">
<Label
htmlFor="grade"
className="text-xs font-bold uppercase text-muted-foreground"
>
{t("ui.admin.users.detail.form.grade", "직급")}
</Label>
<Input
id="grade"
{...register("grade")}
className="h-11 shadow-sm"
/>
</div>
<div className="space-y-2">
<Label
htmlFor="position"
className="text-xs font-bold uppercase text-muted-foreground"
>
{t("ui.admin.users.detail.form.position", "직")}
{t("ui.admin.users.detail.form.position", "직")}
</Label>
<Input
id="position"

View File

@@ -249,9 +249,9 @@ export function UserBulkUploadModal({ onSuccess }: UserBulkUploadModalProps) {
const downloadTemplate = () => {
const headers =
"email,name,phone,role,tenant_slug,department,position,jobTitle,employee_id";
"email,name,phone,role,tenant_slug,department,grade,position,jobTitle,employee_id";
const example =
"user1@example.com,홍길동,010-1234-5678,user,tenant-slug,개발팀,수석,프론트엔드,EMP001";
"user1@example.com,홍길동,010-1234-5678,user,tenant-slug,개발팀,수석,팀장,프론트엔드,EMP001";
const blob = new Blob([`${headers}\n${example}`], {
type: "text/csv;charset=utf-8;",
});

View File

@@ -57,6 +57,7 @@ test@test.com,Test,baron`;
name: "John Doe",
phone: "+19144812222",
department: "myteam",
grade: "Manager",
position: "Manager",
jobTitle: "Sales management",
tenantImport: {

View File

@@ -79,6 +79,8 @@ export function parseUserCSV(text: string): BulkUserItem[] {
};
} else if (header === "department") {
item.department = value;
} else if (header === "grade") {
item.grade = value;
} else if (header === "position") {
item.position = value;
} else if (header === "jobtitle") {
@@ -100,6 +102,7 @@ export function parseUserCSV(text: string): BulkUserItem[] {
} else if (header === "usertype") {
item.metadata.naverworks_user_type = value;
} else if (header === "level") {
item.grade = value;
item.metadata.naverworks_level = value;
} else if (header === "organization") {
item.metadata.naverworks_organization_path = value;
@@ -247,7 +250,7 @@ function applyNaverWorksFallbacks(
item.phone = `${countryCode}${number}`.replace(/\s/g, "");
}
if (!item.position && item.metadata.naverworks_level) {
item.position = item.metadata.naverworks_level;
if (!item.grade && item.metadata.naverworks_level) {
item.grade = item.metadata.naverworks_level;
}
}