diff --git a/.playwright-mcp/page-2026-05-20T02-00-01-354Z.yml b/.playwright-mcp/page-2026-05-20T02-00-01-354Z.yml
new file mode 100644
index 00000000..44680000
--- /dev/null
+++ b/.playwright-mcp/page-2026-05-20T02-00-01-354Z.yml
@@ -0,0 +1,23 @@
+- generic [ref=e4]:
+ - generic [ref=e5]:
+ - img [ref=e7]
+ - generic [ref=e9]:
+ - heading "Baron SSO" [level=1] [ref=e10]
+ - paragraph [ref=e11]: Admin Control Plane
+ - generic [ref=e12]:
+ - generic [ref=e13]:
+ - heading "관리자 로그인" [level=3] [ref=e14]:
+ - img [ref=e15]
+ - text: 관리자 로그인
+ - paragraph [ref=e18]: Baron 통합 인증(SSO)을 통해 관리자 페이지에 접속합니다.
+ - generic [ref=e19]:
+ - button "SSO 계정으로 로그인" [ref=e20] [cursor=pointer]:
+ - img [ref=e21]
+ - text: SSO 계정으로 로그인
+ - img [ref=e23]
+ - paragraph [ref=e27]:
+ - text: 관리자 전역 세션은 보안을 위해 15분간 유지됩니다.
+ - text: 민감한 작업 시 재인증을 요구할 수 있습니다.
+ - paragraph [ref=e32]:
+ - text: 인증 정보가 없거나 로그인이 되지 않는 경우
+ - text: 시스템 관리자에게 문의하세요.
\ No newline at end of file
diff --git a/README.md b/README.md
index 944d0dc4..7922dc66 100644
--- a/README.md
+++ b/README.md
@@ -185,6 +185,7 @@ AdminFront의 테넌트와 사용자 export/import는 운영자가 CSV를 직접
- 기존 `inactive` 입력은 `preboarding`으로, `leave_of_absence` 입력은 `temporary_leave`로 호환 처리합니다.
- 이슈 #862의 초기 명칭 `baron_only`는 구현 명칭으로 사용하지 않고 `baron_guest`로 정리합니다.
+- backend bootstrap은 남아 있는 legacy `users.status` 값을 `inactive -> preboarding`, `leave_of_absence -> temporary_leave`, `baron_only -> baron_guest`로 자동 정규화합니다.
- `archived` 사용자는 과거 이력 보존용 계정이며 AdminFront 같은 관리자 화면에서만 감사/운영/중복 확인 목적으로 조회할 수 있습니다.
diff --git a/adminfront/src/features/tenants/routes/TenantWorksmobilePage.test.ts b/adminfront/src/features/tenants/routes/TenantWorksmobilePage.test.ts
index 6df0b398..68257648 100644
--- a/adminfront/src/features/tenants/routes/TenantWorksmobilePage.test.ts
+++ b/adminfront/src/features/tenants/routes/TenantWorksmobilePage.test.ts
@@ -4,11 +4,14 @@ import {
canCreateWorksmobileRow,
canOpenWorksmobilePasswordManage,
canSelectWorksmobileRow,
+ comparisonFilterOptions,
filterVisibleWorksmobileComparisonRows,
filterWorksmobileComparisonRows,
filterWorksmobileComparisonRowsBySearch,
formatWorksmobileOrgDetails,
formatWorksmobilePersonName,
+ formatWorksmobileUpdateDetails,
+ getDefaultGroupComparisonFilters,
getDefaultWorksmobileComparisonColumns,
getWorksmobileComparisonStatusLabel,
getWorksmobileRowSelectionKey,
@@ -24,6 +27,7 @@ describe("TenantWorksmobilePage comparison helpers", () => {
it("summarizes comparison rows by status", () => {
const summary = summarizeWorksmobileComparison([
{ resourceType: "USER", status: "matched" },
+ { resourceType: "GROUP", status: "needs_update" },
{ resourceType: "USER", status: "missing_in_worksmobile" },
{ resourceType: "USER", status: "missing_in_baron" },
{ resourceType: "USER", status: "missing_external_key" },
@@ -31,8 +35,9 @@ describe("TenantWorksmobilePage comparison helpers", () => {
]);
expect(summary).toEqual({
- total: 5,
+ total: 6,
matched: 1,
+ needsUpdate: 1,
missingInWorksmobile: 1,
missingInBaron: 2,
missingExternalKey: 1,
@@ -50,6 +55,9 @@ describe("TenantWorksmobilePage comparison helpers", () => {
expect(getWorksmobileComparisonStatusLabel("missing_external_key")).toBe(
"ex_key 없음",
);
+ expect(getWorksmobileComparisonStatusLabel("needs_update")).toBe(
+ "업데이트 필요",
+ );
expect(getWorksmobileComparisonStatusLabel("unknown_status")).toBe(
"unknown_status",
);
@@ -426,11 +434,52 @@ describe("TenantWorksmobilePage comparison helpers", () => {
it("orders user comparison filter options from Baron-only first", () => {
expect(userFilterOptions.map((option) => option.value)).toEqual([
"baron_only",
+ "needs_update",
"works_only",
"matched",
]);
});
+ it("keeps all organization/group comparison filter labels available", () => {
+ expect(comparisonFilterOptions).toEqual([
+ { value: "baron_only", label: "바론에만 있음" },
+ { value: "needs_update", label: "업데이트 필요" },
+ { value: "works_only", label: "웍스에만 있음" },
+ { value: "matched", label: "양쪽 다 있음" },
+ ]);
+ });
+
+ it("shows update-needed group rows by default", () => {
+ const rows = [
+ { resourceType: "GROUP", status: "needs_update", baronId: "org-1" },
+ { resourceType: "GROUP", status: "matched", baronId: "org-2" },
+ ];
+
+ expect(
+ filterWorksmobileComparisonRows(rows, getDefaultGroupComparisonFilters()),
+ ).toEqual([rows[0]]);
+ });
+
+ it("formats update details for changed organization rows", () => {
+ expect(
+ formatWorksmobileUpdateDetails({
+ resourceType: "GROUP",
+ status: "needs_update",
+ baronId: "818c856b-9545-442f-b827-d1c569f200b0",
+ baronName: "삼안기술개발센터(조직도용)",
+ worksmobileName: "기술개발센터(조직도용)",
+ baronParentId: "9caf62e1-297d-4e8f-870b-61780998bbeb",
+ baronParentWorksmobileId: "works-saman",
+ baronParentWorksmobileName: "삼안",
+ worksmobileParentId: "works-other",
+ worksmobileParentName: "다른 상위",
+ }),
+ ).toEqual([
+ "이름: 기술개발센터(조직도용) -> 삼안기술개발센터(조직도용)",
+ "상위: 다른 상위 -> 삼안",
+ ]);
+ });
+
it("formats WORKS account name with level on one line", () => {
expect(
formatWorksmobilePersonName({
diff --git a/adminfront/src/features/tenants/routes/TenantWorksmobilePage.tsx b/adminfront/src/features/tenants/routes/TenantWorksmobilePage.tsx
index 08595d58..a8af2063 100644
--- a/adminfront/src/features/tenants/routes/TenantWorksmobilePage.tsx
+++ b/adminfront/src/features/tenants/routes/TenantWorksmobilePage.tsx
@@ -63,6 +63,8 @@ import {
filterWorksmobileComparisonRowsBySearch,
formatWorksmobileOrgDetails,
formatWorksmobilePersonName,
+ formatWorksmobileUpdateDetails,
+ getDefaultGroupComparisonFilters,
getDefaultWorksmobileComparisonColumns,
getWorksmobileComparisonStatusLabel,
getWorksmobileRowSelectionKey,
@@ -81,7 +83,7 @@ export function TenantWorksmobilePage() {
>(["baron_only", "works_only"]);
const [groupFilters, setGroupFilters] = React.useState<
WorksmobileComparisonFilter[]
- >(["baron_only", "works_only"]);
+ >(getDefaultGroupComparisonFilters);
const [includeUserMissingExternalKey, setIncludeUserMissingExternalKey] =
React.useState(false);
const [includeGroupMissingExternalKey, setIncludeGroupMissingExternalKey] =
@@ -594,6 +596,9 @@ function getWorksmobileComparisonStatusVariant(status: string) {
if (status === "matched") {
return "success";
}
+ if (status === "needs_update") {
+ return "warning";
+ }
if (status === "missing_external_key") {
return "warning";
}
@@ -622,6 +627,10 @@ function ComparisonSummary({
Baron 없음
{summary.missingInBaron}
+
+ 업데이트 필요
+ {summary.needsUpdate}
+
ex_key 없음
{summary.missingExternalKey}
@@ -860,7 +869,7 @@ function ComparisonTable({
return (
-
+
{title}
-
+