1
0
forked from baron/baron-sso

네이버 계정 정합성 맞춤

This commit is contained in:
2026-06-15 19:54:09 +09:00
parent 8e9d015443
commit 4d468cd39f
97 changed files with 5837 additions and 2031 deletions

View File

@@ -61,6 +61,13 @@ function TenantCreatePage() {
});
const tenants = parentQuery.data?.items ?? [];
const selectedParentTenant = tenants.find((tenant) => tenant.id === parentId);
const hanmacFamilyTenantId = useMemo(
() =>
tenants.find(
(tenant) => tenant.slug.trim().toLowerCase() === "hanmac-family",
)?.id ?? "",
[tenants],
);
const canConfigureHanmacOrg = useMemo(() => {
if (!selectedParentTenant) return false;
if (selectedParentTenant.slug.toLowerCase() === "hanmac-family") {
@@ -206,6 +213,7 @@ function TenantCreatePage() {
"ui.admin.tenants.create.form.pick_hanmac_parent",
"한맥가족에서 선택",
)}
orgChartTenantId={hanmacFamilyTenantId}
localPickerLabel={t(
"ui.admin.tenants.create.form.pick_other_parent",
"다른 테넌트 선택",

View File

@@ -125,7 +125,7 @@ function TenantDetailPage() {
</div>
{/* Outlet for nested routes */}
<div className="animate-in fade-in duration-500">
<div>
<Outlet />
</div>
</div>

View File

@@ -1,3 +1,5 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
buildWorksmobilePasswordManageUrl,
@@ -28,6 +30,18 @@ import {
} from "./worksmobileComparison";
describe("TenantWorksmobilePage comparison helpers", () => {
it("does not apply page-level enter animations to Worksmobile tab panels", () => {
const source = readFileSync(
join(
process.cwd(),
"src/features/tenants/routes/TenantWorksmobilePage.tsx",
),
"utf8",
);
expect(source).not.toContain("space-y-4 animate-in fade-in duration-500");
});
it("summarizes comparison rows by status", () => {
const summary = summarizeWorksmobileComparison([
{ resourceType: "USER", status: "matched" },
@@ -594,6 +608,41 @@ describe("TenantWorksmobilePage comparison helpers", () => {
]);
});
it("formats backend update reasons when value diff details are not directly visible", () => {
expect(
formatWorksmobileUpdateDetails({
resourceType: "USER",
status: "needs_update",
baronId: "user-1",
baronName: "신현우",
worksmobileName: "신현우",
baronEmail: "hwshin2@hanmaceng.co.kr",
worksmobileEmail: "hwshin2@hanmaceng.co.kr",
externalKey: "user-1",
updateReasons: ["organization"],
}),
).toEqual(["조직: Baron 소속 정보를 WORKS에 반영해야 합니다."]);
});
it("does not format phone update details for spaced Korean country code formatting only", () => {
expect(
formatWorksmobileUpdateDetails({
resourceType: "USER",
status: "needs_update",
baronId: "user-1",
baronName: "강명진",
worksmobileName: "강명진",
baronEmail: "mjkang4@hanmaceng.co.kr",
worksmobileEmail: "mjkang4@hanmaceng.co.kr",
externalKey: "user-1",
baronPhone: "+821041585840",
worksmobilePhone: "+82 1041585840",
baronEmployeeNumber: "mjkang4",
worksmobileEmployeeNumber: "M17205",
}),
).toEqual(["사번: M17205 -> mjkang4"]);
});
it("formats WORKS account name with level on one line", () => {
expect(
formatWorksmobilePersonName({

View File

@@ -520,7 +520,7 @@ export function TenantWorksmobilePage() {
</div>
{activeTab === "history" ? (
<div className="space-y-4 animate-in fade-in duration-500">
<div className="space-y-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between gap-3">
<div>
@@ -627,7 +627,7 @@ export function TenantWorksmobilePage() {
) : null}
{activeTab === "users" ? (
<div className="space-y-4 animate-in fade-in duration-500">
<div className="space-y-4">
<ComparisonSummary
title={t(
"ui.admin.tenants.worksmobile.compare",
@@ -715,7 +715,7 @@ export function TenantWorksmobilePage() {
) : null}
{activeTab === "groups" ? (
<div className="space-y-4 animate-in fade-in duration-500">
<div className="space-y-4">
<ComparisonSummary
title={t(
"ui.admin.tenants.worksmobile.compare_groups",

View File

@@ -374,28 +374,39 @@ export function formatWorksmobileUpdateDetails(row: WorksmobileComparisonItem) {
}
const details: string[] = [];
const renderedReasons = new Set<string>();
const addDetail = (reason: string, detail: string) => {
details.push(detail);
renderedReasons.add(reason);
};
const baronName = row.baronName?.trim();
const worksmobileName = row.worksmobileName?.trim();
if (baronName && worksmobileName && baronName !== worksmobileName) {
details.push(`이름: ${worksmobileName} -> ${baronName}`);
addDetail("name", `이름: ${worksmobileName} -> ${baronName}`);
}
if (row.resourceType === "USER") {
const expectedExternalKey = row.baronId?.trim() ?? "";
const actualExternalKey = row.externalKey?.trim() ?? "";
if (expectedExternalKey && expectedExternalKey !== actualExternalKey) {
details.push(
addDetail(
"external_key",
`external_key: ${actualExternalKey || "없음"} -> ${expectedExternalKey}`,
);
}
const expectedEmail = row.baronEmail?.trim().toLowerCase() ?? "";
const actualEmail = row.worksmobileEmail?.trim().toLowerCase() ?? "";
if (expectedEmail && actualEmail && expectedEmail !== actualEmail) {
details.push(`이메일: ${actualEmail} -> ${expectedEmail}`);
addDetail("email", `이메일: ${actualEmail} -> ${expectedEmail}`);
}
const expectedPhone = row.baronPhone?.trim() ?? "";
const actualPhone = row.worksmobilePhone?.trim() ?? "";
if (expectedPhone && actualPhone && expectedPhone !== actualPhone) {
details.push(`전화번호: ${actualPhone} -> ${expectedPhone}`);
if (
expectedPhone &&
actualPhone &&
normalizeWorksmobilePhoneForCompare(expectedPhone) !==
normalizeWorksmobilePhoneForCompare(actualPhone)
) {
addDetail("phone", `전화번호: ${actualPhone} -> ${expectedPhone}`);
}
const expectedEmployeeNumber = row.baronEmployeeNumber?.trim() ?? "";
const actualEmployeeNumber = row.worksmobileEmployeeNumber?.trim() ?? "";
@@ -404,10 +415,12 @@ export function formatWorksmobileUpdateDetails(row: WorksmobileComparisonItem) {
actualEmployeeNumber &&
expectedEmployeeNumber !== actualEmployeeNumber
) {
details.push(
addDetail(
"employee_number",
`사번: ${actualEmployeeNumber} -> ${expectedEmployeeNumber}`,
);
}
appendWorksmobileUpdateReasonFallbacks(details, row, renderedReasons);
return details;
}
@@ -427,14 +440,86 @@ export function formatWorksmobileUpdateDetails(row: WorksmobileComparisonItem) {
const actualParentKey =
row.worksmobileParentId ?? row.worksmobileParentExternalKey ?? "";
if (expectedParentKey !== actualParentKey) {
details.push(
addDetail(
"organization",
`상위: ${actualParent || "없음"} -> ${expectedParent || "없음"}`,
);
}
appendWorksmobileUpdateReasonFallbacks(details, row, renderedReasons);
return details;
}
function appendWorksmobileUpdateReasonFallbacks(
details: string[],
row: WorksmobileComparisonItem,
renderedReasons: Set<string>,
) {
for (const reason of row.updateReasons ?? []) {
const normalizedReason = reason.trim();
if (!normalizedReason || renderedReasons.has(normalizedReason)) {
continue;
}
const detail = formatWorksmobileUpdateReasonFallback(normalizedReason, row);
if (!detail) {
continue;
}
details.push(detail);
renderedReasons.add(normalizedReason);
}
}
function formatWorksmobileUpdateReasonFallback(
reason: string,
row: WorksmobileComparisonItem,
) {
switch (reason) {
case "name":
return "이름: Baron 사용자명을 WORKS에 반영해야 합니다.";
case "external_key":
return "external_key: Baron 사용자 ID를 WORKS 외부 키로 반영해야 합니다.";
case "email":
return "이메일: Baron 이메일을 WORKS에 반영해야 합니다.";
case "phone":
return "전화번호: Baron 전화번호를 WORKS에 반영해야 합니다.";
case "employee_number":
return "사번: Baron 사번을 WORKS에 반영해야 합니다.";
case "organization":
return row.resourceType === "GROUP"
? "조직: Baron 조직 정보를 WORKS에 반영해야 합니다."
: "조직: Baron 소속 정보를 WORKS에 반영해야 합니다.";
case "manager":
return "조직장: Baron 조직장 설정을 WORKS에 반영해야 합니다.";
default:
return `업데이트 사유: ${reason}`;
}
}
function normalizeWorksmobilePhoneForCompare(value: string) {
const trimmed = value.trim();
if (!trimmed) {
return "";
}
const digits = trimmed.replace(/\D/g, "");
if (!digits) {
return "";
}
if (digits.startsWith("010")) {
return `+82${digits.slice(1)}`;
}
if (digits.startsWith("82")) {
let rest = digits.slice(2);
while (rest.startsWith("82")) {
rest = rest.slice(2);
}
if (rest.startsWith("0")) {
rest = rest.slice(1);
}
return `+82${rest}`;
}
return `+${digits}`;
}
export function buildWorksmobilePasswordManageUrl({
tenantId,
domainId,