forked from baron/baron-sso
worksmobile 연동 & ory stack 26.2.0으로 업그레이드
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -32,7 +32,13 @@ import {
|
||||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { Switch } from "../../components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -55,6 +61,7 @@ import {
|
||||
} from "../../lib/adminApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { UserBulkUploadModal } from "./components/UserBulkUploadModal";
|
||||
import { userStatusLabel, userStatusValues } from "./userStatus";
|
||||
|
||||
type UserSchemaField = {
|
||||
key: string;
|
||||
@@ -579,28 +586,40 @@ function UserListPage() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={user.status === "active"}
|
||||
onCheckedChange={(checked) =>
|
||||
<Select
|
||||
value={user.status}
|
||||
onValueChange={(status) =>
|
||||
statusMutation.mutate({
|
||||
userId: user.id,
|
||||
status: checked ? "active" : "inactive",
|
||||
status,
|
||||
})
|
||||
}
|
||||
disabled={
|
||||
statusMutation.isPending ||
|
||||
user.id === profile?.id
|
||||
}
|
||||
aria-label={t(
|
||||
"ui.admin.users.list.toggle_status",
|
||||
"{{name}} 활성 상태",
|
||||
{ name: user.name },
|
||||
)}
|
||||
data-testid={`user-status-toggle-${user.id}`}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t(`ui.common.status.${user.status}`, user.status)}
|
||||
</span>
|
||||
>
|
||||
<SelectTrigger
|
||||
className="h-8 w-36"
|
||||
aria-label={t(
|
||||
"ui.admin.users.list.status_select",
|
||||
"{{name}} 상태",
|
||||
{ name: user.name },
|
||||
)}
|
||||
data-testid={`user-status-select-${user.id}`}
|
||||
>
|
||||
<SelectValue>
|
||||
{userStatusLabel(user.status)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{userStatusValues.map((status) => (
|
||||
<SelectItem key={status} value={status}>
|
||||
{userStatusLabel(status)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</TableCell>
|
||||
{/* Dynamic Metadata Cells */}
|
||||
@@ -683,6 +702,24 @@ function UserListPage() {
|
||||
>
|
||||
{t("ui.common.status.inactive", "비활성화")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-background hover:bg-background/10 h-8"
|
||||
onClick={() => handleBulkStatusChange("suspended")}
|
||||
data-testid="bulk-suspended-btn"
|
||||
>
|
||||
{t("ui.common.status.suspended", "정지")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-background hover:bg-background/10 h-8"
|
||||
onClick={() => handleBulkStatusChange("leave_of_absence")}
|
||||
data-testid="bulk-leave-of-absence-btn"
|
||||
>
|
||||
{t("ui.common.status.leave_of_absence", "휴직")}
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-background/20 mx-1" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
14
adminfront/src/features/users/userStatus.ts
Normal file
14
adminfront/src/features/users/userStatus.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
export const userStatusValues = [
|
||||
"active",
|
||||
"inactive",
|
||||
"suspended",
|
||||
"leave_of_absence",
|
||||
] as const;
|
||||
|
||||
export type UserStatusValue = (typeof userStatusValues)[number];
|
||||
|
||||
export function userStatusLabel(status: string) {
|
||||
return t(`ui.common.status.${status}`, status);
|
||||
}
|
||||
@@ -44,6 +44,36 @@ test@test.com,Test,baron`;
|
||||
expect(result[0].tenantSlug).toBe("baron");
|
||||
});
|
||||
|
||||
it("should parse NAVERWORKS member CSV sample into Baron bulk user fields", () => {
|
||||
const csv = `"LastName","FirstName","ID","Personal email","Sub email","Nickname","User type","Level","Organization","Position","CompanyMainPhone","Mobile/Country code","Mobile/Numbers","Language","Responsibilities","Workplace","SNS","SNS_ID","Birthday (solar, lunar)","Birthday","Entry Date","Employee number","Account activation time"
|
||||
"Doe","John","john.doe","john@naver.com","john1@company.com; john2@company.com","John","Permanent Employee","Manager","org.1|org.2|org.3|myteam","Manager","02-0000-0000","+1","9144812222","English","Sales management","New York","Facebook","john","solar","19830415","20230415","AB001","20230415 08:00"`;
|
||||
|
||||
const result = parseUserCSV(csv);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
email: "john1@company.com",
|
||||
loginId: "john.doe",
|
||||
name: "John Doe",
|
||||
phone: "+19144812222",
|
||||
department: "myteam",
|
||||
position: "Manager",
|
||||
jobTitle: "Sales management",
|
||||
tenantImport: {
|
||||
name: "myteam",
|
||||
parentTenantName: "org.3",
|
||||
},
|
||||
metadata: {
|
||||
personal_email: "john@naver.com",
|
||||
employee_id: "AB001",
|
||||
naverworks_user_type: "Permanent Employee",
|
||||
naverworks_level: "Manager",
|
||||
naverworks_organization_path: "org.1|org.2|org.3|myteam",
|
||||
naverworks_workplace: "New York",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse tenant conflict metadata for import resolution", () => {
|
||||
const csv = `email,name,tenant_id,tenant_slug,tenant_name,tenant_type,parent_tenant_slug,tenant_memo,email_domain
|
||||
test@test.com,Test,local-tenant-id,missing-slug,Missing Tenant,COMPANY,parent-slug,Imported memo,missing.example.com`;
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import type { BulkUserItem } from "../../../lib/adminApi";
|
||||
|
||||
export function parseUserCSV(text: string): BulkUserItem[] {
|
||||
const lines = text.split(/\r?\n/);
|
||||
if (lines.length < 2) {
|
||||
const records = parseCSVRecords(text.replace(/^\uFEFF/, ""));
|
||||
if (records.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const headers = lines[0].split(",").map((h) => h.trim().toLowerCase());
|
||||
const headers = records[0].map(normalizeHeader);
|
||||
const data: BulkUserItem[] = [];
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
if (!lines[i].trim()) continue;
|
||||
|
||||
const values = lines[i].split(",").map((v) => v.trim());
|
||||
for (let i = 1; i < records.length; i++) {
|
||||
const values = records[i].map((v) => v.trim());
|
||||
if (values.every((value) => value === "")) continue;
|
||||
const item: Partial<BulkUserItem> & { metadata: Record<string, string> } = {
|
||||
metadata: {},
|
||||
};
|
||||
@@ -84,11 +83,70 @@ export function parseUserCSV(text: string): BulkUserItem[] {
|
||||
item.position = value;
|
||||
} else if (header === "jobtitle") {
|
||||
item.jobTitle = value;
|
||||
} else if (header === "lastname") {
|
||||
item.metadata.naverworks_last_name = value;
|
||||
} else if (header === "firstname") {
|
||||
item.metadata.naverworks_first_name = value;
|
||||
} else if (header === "id") {
|
||||
item.loginId = value;
|
||||
item.metadata.naverworks_id = value;
|
||||
} else if (header === "personalemail") {
|
||||
item.metadata.personal_email = value;
|
||||
} else if (header === "subemail") {
|
||||
item.metadata.naverworks_sub_email = value;
|
||||
item.email = firstEmailToken(value) || item.email;
|
||||
} else if (header === "nickname") {
|
||||
item.metadata.naverworks_nickname = value;
|
||||
} else if (header === "usertype") {
|
||||
item.metadata.naverworks_user_type = value;
|
||||
} else if (header === "level") {
|
||||
item.metadata.naverworks_level = value;
|
||||
} else if (header === "organization") {
|
||||
item.metadata.naverworks_organization_path = value;
|
||||
const parts = splitOrganizationPath(value);
|
||||
const leaf = parts.at(-1) ?? "";
|
||||
const parent = parts.at(-2) ?? "";
|
||||
if (leaf) {
|
||||
item.department = leaf;
|
||||
item.tenantImport = {
|
||||
...(item.tenantImport ?? {}),
|
||||
name: leaf,
|
||||
parentTenantName: parent,
|
||||
};
|
||||
}
|
||||
} else if (header === "companymainphone") {
|
||||
item.metadata.naverworks_company_main_phone = value;
|
||||
} else if (header === "mobilecountrycode") {
|
||||
item.metadata.naverworks_mobile_country_code = value;
|
||||
} else if (header === "mobilenumbers") {
|
||||
item.metadata.naverworks_mobile_numbers = value;
|
||||
} else if (header === "language") {
|
||||
item.metadata.naverworks_language = value;
|
||||
} else if (header === "responsibilities") {
|
||||
item.jobTitle = value;
|
||||
} else if (header === "workplace") {
|
||||
item.metadata.naverworks_workplace = value;
|
||||
} else if (header === "sns") {
|
||||
item.metadata.naverworks_sns = value;
|
||||
} else if (header === "snsid") {
|
||||
item.metadata.naverworks_sns_id = value;
|
||||
} else if (header === "birthdaysolarlunar") {
|
||||
item.metadata.naverworks_birthday_calendar = value;
|
||||
} else if (header === "birthday") {
|
||||
item.metadata.naverworks_birthday = value;
|
||||
} else if (header === "entrydate") {
|
||||
item.metadata.naverworks_entry_date = value;
|
||||
} else if (header === "employeenumber") {
|
||||
item.metadata.employee_id = value;
|
||||
} else if (header === "accountactivationtime") {
|
||||
item.metadata.naverworks_account_activation_time = value;
|
||||
} else {
|
||||
item.metadata[header] = value;
|
||||
}
|
||||
}
|
||||
|
||||
applyNaverWorksFallbacks(item);
|
||||
|
||||
if (item.email && item.name) {
|
||||
data.push(item as BulkUserItem);
|
||||
}
|
||||
@@ -96,3 +154,100 @@ export function parseUserCSV(text: string): BulkUserItem[] {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function normalizeHeader(header: string) {
|
||||
return header
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^\uFEFF/, "")
|
||||
.replace(/[^a-z0-9_]/g, "");
|
||||
}
|
||||
|
||||
function parseCSVRecords(text: string) {
|
||||
const records: string[][] = [];
|
||||
let field = "";
|
||||
let row: string[] = [];
|
||||
let quoted = false;
|
||||
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
const char = text[index];
|
||||
const next = text[index + 1];
|
||||
|
||||
if (char === '"') {
|
||||
if (quoted && next === '"') {
|
||||
field += '"';
|
||||
index++;
|
||||
} else {
|
||||
quoted = !quoted;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "," && !quoted) {
|
||||
row.push(field);
|
||||
field = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((char === "\n" || char === "\r") && !quoted) {
|
||||
if (char === "\r" && next === "\n") index++;
|
||||
row.push(field);
|
||||
records.push(row);
|
||||
field = "";
|
||||
row = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
field += char;
|
||||
}
|
||||
|
||||
if (field !== "" || row.length > 0) {
|
||||
row.push(field);
|
||||
records.push(row);
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
function firstEmailToken(value: string) {
|
||||
return (
|
||||
value
|
||||
.split(/[;,]/)
|
||||
.map((token) => token.trim())
|
||||
.find((token) => token.includes("@")) ?? ""
|
||||
);
|
||||
}
|
||||
|
||||
function splitOrganizationPath(value: string) {
|
||||
return value
|
||||
.split("|")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function applyNaverWorksFallbacks(
|
||||
item: Partial<BulkUserItem> & { metadata: Record<string, string> },
|
||||
) {
|
||||
if (!item.name) {
|
||||
const firstName = item.metadata.naverworks_first_name ?? "";
|
||||
const lastName = item.metadata.naverworks_last_name ?? "";
|
||||
item.name = [firstName, lastName].filter(Boolean).join(" ").trim();
|
||||
if (!item.name && item.metadata.naverworks_nickname) {
|
||||
item.name = item.metadata.naverworks_nickname;
|
||||
}
|
||||
}
|
||||
|
||||
if (!item.email) {
|
||||
item.email = item.metadata.personal_email;
|
||||
}
|
||||
|
||||
if (!item.phone) {
|
||||
const countryCode = item.metadata.naverworks_mobile_country_code ?? "";
|
||||
const number = item.metadata.naverworks_mobile_numbers ?? "";
|
||||
item.phone = `${countryCode}${number}`.replace(/\s/g, "");
|
||||
}
|
||||
|
||||
if (!item.position && item.metadata.naverworks_level) {
|
||||
item.position = item.metadata.naverworks_level;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user