From 46ef6d8ffb50628f04db2fa0ac71a2c6524ddd6e Mon Sep 17 00:00:00 2001 From: kangcheolung Date: Fri, 21 Aug 2026 03:13:39 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EC=A7=81=EC=A0=91=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=ED=9A=8C=EC=88=98=EC=97=90=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EC=A0=88=EC=B0=A8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 대상·리소스 이름·권한 종류를 확인창에 표시하고, 회수 버튼 확인 없이 바로 DELETE가 실행되던 문제를 고친다. targetLabel도 ROLE/DEPARTMENT는 ID 대신 실제 이름을 보여주도록 개선한다. Co-Authored-By: Claude Sonnet 5 --- frontend/app/features/PermissionsPage.tsx | 25 +++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/frontend/app/features/PermissionsPage.tsx b/frontend/app/features/PermissionsPage.tsx index 14f8c462..e25e926a 100644 --- a/frontend/app/features/PermissionsPage.tsx +++ b/frontend/app/features/PermissionsPage.tsx @@ -94,11 +94,17 @@ export function PermissionsPage({ notify }: { notify: (message: string) => void finally { setBusy(false); } } - async function revoke(permissionId: number) { + async function revoke(permission: PermissionGrant) { if (!resourceId) return; + const target = targetLabel(permission, roles, departments); + const resourceName = resourceType === "documents" + ? documents.find((item) => item.documentId === Number(resourceId))?.title + : collections.find((item) => item.collectionId === Number(resourceId))?.name; + const resourceLabel = resourceName ?? `${resourceType === "documents" ? "문서" : "컬렉션"} #${resourceId}`; + if (!window.confirm(`${resourceLabel}에서 ${target}의 ${permission.permissionType} 권한을 회수할까요?`)) return; setBusy(true); try { - await apiRequest(`/permissions/${resourceType}/${resourceId}/${permissionId}`, { method: "DELETE" }); + await apiRequest(`/permissions/${resourceType}/${resourceId}/${permission.permissionId}`, { method: "DELETE" }); notify("권한을 회수했습니다."); await loadDirectPermissions(); } catch (reason) { setError(errorMessage(reason)); } @@ -132,13 +138,20 @@ export function PermissionsPage({ notify }: { notify: (message: string) => void {permissionsLoading ? : null} {!permissionsLoading && permissionsError ? 직접 권한 목록을 조회할 수 없습니다. {permissionsError} : null} {!permissionsLoading && !permissionsError && !directPermissions.length ? 이 리소스에 직접 부여된 권한이 없습니다. : null} - {!permissionsLoading && directPermissions.length ?
대상권한만료
{directPermissions.map((permission) =>
{targetLabel(permission)}#{permission.permissionId} · user #{permission.grantedBy} · {formatDate(permission.grantedAt)}{permission.expiresAt ? formatDate(permission.expiresAt) : "제한 없음"}
)}
: null} + {!permissionsLoading && directPermissions.length ?
대상권한만료
{directPermissions.map((permission) =>
{targetLabel(permission, roles, departments)}#{permission.permissionId} · user #{permission.grantedBy} · {formatDate(permission.grantedAt)}{permission.expiresAt ? formatDate(permission.expiresAt) : "제한 없음"}
)}
: null} : null} ; } -function targetLabel(permission: PermissionGrant) { - const targetId = permission.userId ?? permission.roleId ?? permission.departmentId; - return `${permission.targetType} #${targetId ?? "—"}`; +function targetLabel(permission: PermissionGrant, roles: Role[], departments: Department[]) { + if (permission.targetType === "ROLE") { + const role = roles.find((item) => item.id === permission.roleId); + return role ? `ROLE ${role.name}` : `ROLE #${permission.roleId ?? "—"}`; + } + if (permission.targetType === "DEPARTMENT") { + const department = departments.find((item) => item.id === permission.departmentId); + return department ? `DEPARTMENT ${department.name}` : `DEPARTMENT #${permission.departmentId ?? "—"}`; + } + return `USER #${permission.userId ?? "—"}`; } From 4795a4dd916d9adb42ae5f9158509c61d212aa6c Mon Sep 17 00:00:00 2001 From: kangcheolung Date: Fri, 21 Aug 2026 03:19:38 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EA=B6=8C=ED=95=9C=20=ED=9A=8C?= =?UTF-8?q?=EC=88=98=20=EC=84=B1=EA=B3=B5=20=EC=8B=9C=20=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EC=83=81=ED=83=9C=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit 리뷰 반영. revoke()가 성공해도 error를 비우지 않아 직전 실패(예: 권한 부여 실패)의 에러 배너가 회수 성공 후에도 남아있던 문제를 고친다. Co-Authored-By: Claude Sonnet 5 --- frontend/app/features/PermissionsPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/app/features/PermissionsPage.tsx b/frontend/app/features/PermissionsPage.tsx index e25e926a..83a39e4c 100644 --- a/frontend/app/features/PermissionsPage.tsx +++ b/frontend/app/features/PermissionsPage.tsx @@ -103,6 +103,7 @@ export function PermissionsPage({ notify }: { notify: (message: string) => void const resourceLabel = resourceName ?? `${resourceType === "documents" ? "문서" : "컬렉션"} #${resourceId}`; if (!window.confirm(`${resourceLabel}에서 ${target}의 ${permission.permissionType} 권한을 회수할까요?`)) return; setBusy(true); + setError(""); try { await apiRequest(`/permissions/${resourceType}/${resourceId}/${permission.permissionId}`, { method: "DELETE" }); notify("권한을 회수했습니다.");