Implement multi-language support in user management and system management pages

- Integrated multi-language functionality across various user management components, including user list, roles list, and user authorization pages, enhancing accessibility for diverse users.
- Updated UI elements to utilize translation keys, ensuring that all text is dynamically translated based on user preferences.
- Improved error handling messages to be localized, providing a better user experience in case of issues.

These changes significantly enhance the usability and internationalization of the user management features, making the application more inclusive.
This commit is contained in:
kjs
2026-04-01 15:57:12 +09:00
parent 2ff01456dc
commit 1d49fc7ac7
15 changed files with 812 additions and 200 deletions

View File

@@ -10,6 +10,109 @@ import { ScrollToTop } from "@/components/common/ScrollToTop";
import { useAuth } from "@/hooks/useAuth";
import { AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { usePageMultiLang } from "@/hooks/usePageMultiLang";
// 다국어 키 목록
const LANG_KEYS = [
"company.page.title",
"company.page.description",
"company.access.denied.title",
"company.access.denied.description",
"company.access.denied.back",
"company.toolbar.total",
"company.toolbar.create",
"company.table.companyCode",
"company.table.companyName",
"company.table.writer",
"company.table.diskUsage",
"company.table.diskNoInfo",
"company.table.fileCount",
"company.table.actions",
"company.table.empty",
"company.table.cardWriter",
"company.table.cardDiskUsage",
"company.table.actionDept",
"company.table.actionEdit",
"company.table.actionDelete",
"company.form.titleCreate",
"company.form.titleEdit",
"company.form.companyName",
"company.form.companyNamePlaceholder",
"company.form.businessNumber",
"company.form.businessNumberHint",
"company.form.representativeName",
"company.form.representativeNamePlaceholder",
"company.form.representativePhone",
"company.form.email",
"company.form.website",
"company.form.address",
"company.form.addressPlaceholder",
"company.form.companyCodeLabel",
"company.form.writerLabel",
"company.form.regdateLabel",
"company.form.cancel",
"company.form.save",
"company.form.update",
"company.delete.title",
"company.delete.description",
"company.delete.companyName",
"company.delete.companyCode",
"company.delete.writer",
"company.delete.regdate",
"company.delete.cancel",
"company.delete.confirm",
] as const;
// 한국어 기본 텍스트
const DEFAULT_TEXTS: Record<string, string> = {
"company.page.title": "회사 관리",
"company.page.description": "시스템에서 사용하는 회사 정보를 관리합니다",
"company.access.denied.title": "접근 권한 없음",
"company.access.denied.description": "회사 관리는 최고 관리자만 접근할 수 있습니다.",
"company.access.denied.back": "뒤로 가기",
"company.toolbar.total": "총",
"company.toolbar.create": "회사 등록",
"company.table.companyCode": "회사코드",
"company.table.companyName": "회사명",
"company.table.writer": "등록자",
"company.table.diskUsage": "디스크 사용량",
"company.table.diskNoInfo": "정보 없음",
"company.table.fileCount": "{count}개 파일",
"company.table.actions": "작업",
"company.table.empty": "등록된 회사가 없습니다.",
"company.table.cardWriter": "작성자",
"company.table.cardDiskUsage": "디스크 사용량",
"company.table.actionDept": "부서관리",
"company.table.actionEdit": "수정",
"company.table.actionDelete": "삭제",
"company.form.titleCreate": "새 회사 등록",
"company.form.titleEdit": "회사 정보 수정",
"company.form.companyName": "회사명",
"company.form.companyNamePlaceholder": "회사명을 입력하세요",
"company.form.businessNumber": "사업자등록번호",
"company.form.businessNumberHint": "10자리 숫자 (자동 하이픈 추가)",
"company.form.representativeName": "대표자명",
"company.form.representativeNamePlaceholder": "대표자명을 입력하세요",
"company.form.representativePhone": "대표 연락처",
"company.form.email": "이메일",
"company.form.website": "웹사이트",
"company.form.address": "회사 주소",
"company.form.addressPlaceholder": "서울특별시 강남구...",
"company.form.companyCodeLabel": "회사 코드:",
"company.form.writerLabel": "등록자:",
"company.form.regdateLabel": "등록일:",
"company.form.cancel": "취소",
"company.form.save": "등록",
"company.form.update": "수정",
"company.delete.title": "회사 삭제 확인",
"company.delete.description": "선택한 회사를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"company.delete.companyName": "회사명",
"company.delete.companyCode": "회사 코드",
"company.delete.writer": "등록자",
"company.delete.regdate": "등록일",
"company.delete.cancel": "취소",
"company.delete.confirm": "삭제",
};
/**
* 회사 관리 페이지
@@ -19,6 +122,12 @@ export default function CompanyPage() {
const { user: currentUser } = useAuth();
const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN";
const { t } = usePageMultiLang({
keys: LANG_KEYS,
defaults: DEFAULT_TEXTS,
menuCode: "company.management",
});
const {
// 데이터
companies,
@@ -62,17 +171,17 @@ export default function CompanyPage() {
<div className="flex h-full flex-col bg-background">
<div className="space-y-6 p-6">
<div className="space-y-2 border-b pb-4">
<h1 className="text-3xl font-bold tracking-tight"> </h1>
<p className="text-sm text-muted-foreground"> </p>
<h1 className="text-3xl font-bold tracking-tight">{t("company.page.title")}</h1>
<p className="text-sm text-muted-foreground">{t("company.page.description")}</p>
</div>
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border p-6 shadow-sm">
<AlertCircle className="text-destructive mb-4 h-12 w-12" />
<h3 className="mb-2 text-lg font-semibold"> </h3>
<h3 className="mb-2 text-lg font-semibold">{t("company.access.denied.title")}</h3>
<p className="text-muted-foreground mb-4 text-center text-sm">
.
{t("company.access.denied.description")}
</p>
<Button variant="outline" onClick={() => window.history.back()}>
{t("company.access.denied.back")}
</Button>
</div>
</div>
@@ -85,8 +194,8 @@ export default function CompanyPage() {
<div className="space-y-6 p-4 sm:p-6 lg:p-8">
{/* 페이지 헤더 */}
<div className="space-y-2 border-b pb-4">
<h1 className="text-3xl font-bold tracking-tight"> </h1>
<p className="text-sm text-muted-foreground"> </p>
<h1 className="text-3xl font-bold tracking-tight">{t("company.page.title")}</h1>
<p className="text-sm text-muted-foreground">{t("company.page.description")}</p>
</div>
{/* 디스크 사용량 요약 */}
@@ -100,10 +209,11 @@ export default function CompanyPage() {
onSearchChange={updateSearchFilter}
onSearchClear={clearSearchFilter}
onCreateClick={openCreateModal}
t={t}
/>
{/* 회사 목록 테이블 */}
<CompanyTable companies={companies} isLoading={isLoading} onEdit={openEditModal} onDelete={openDeleteDialog} />
<CompanyTable companies={companies} isLoading={isLoading} onEdit={openEditModal} onDelete={openDeleteDialog} t={t} />
{/* 회사 등록/수정 모달 */}
<CompanyFormModal
@@ -114,6 +224,7 @@ export default function CompanyPage() {
onSave={saveCompany}
onFormChange={updateFormData}
onClearError={clearError}
t={t}
/>
{/* 회사 삭제 확인 다이얼로그 */}
@@ -124,6 +235,7 @@ export default function CompanyPage() {
onClose={closeDeleteDialog}
onConfirm={deleteCompany}
onClearError={clearError}
t={t}
/>
</div>

View File

@@ -11,6 +11,81 @@ import { DualListBox } from "@/components/common/DualListBox";
import { MenuPermissionsTable } from "@/components/admin/MenuPermissionsTable";
import { useMenu } from "@/contexts/MenuContext";
import { ScrollToTop } from "@/components/common/ScrollToTop";
import { usePageMultiLang } from "@/hooks/usePageMultiLang";
const LANG_KEYS = [
"detail.loading",
"detail.error.title",
"detail.error.load",
"detail.error.load.generic",
"detail.error.notfound",
"detail.button.backToList",
"detail.status.active",
"detail.status.inactive",
"detail.tab.members",
"detail.tab.permissions",
"detail.members.title",
"detail.members.description",
"detail.members.mode.user",
"detail.members.mode.dept",
"detail.members.saving",
"detail.members.save",
"detail.members.available",
"detail.members.selected",
"detail.members.dept.available",
"detail.members.dept.selected",
"detail.members.summary",
"detail.members.summary.count",
"detail.members.empty",
"detail.members.save.success",
"detail.members.save.fail",
"detail.members.save.error",
"detail.permissions.title",
"detail.permissions.description",
"detail.permissions.saving",
"detail.permissions.save",
"detail.permissions.save.success",
"detail.permissions.save.fail",
"detail.permissions.save.error",
"detail.count.members",
] as const;
const DEFAULT_TEXTS: Record<string, string> = {
"detail.loading": "권한 그룹 정보를 불러오는 중...",
"detail.error.title": "오류 발생",
"detail.error.load": "권한 그룹 정보를 불러오는데 실패했습니다.",
"detail.error.load.generic": "권한 그룹 정보를 불러오는 중 오류가 발생했습니다.",
"detail.error.notfound": "권한 그룹을 찾을 수 없습니다.",
"detail.button.backToList": "목록으로 돌아가기",
"detail.status.active": "활성",
"detail.status.inactive": "비활성",
"detail.tab.members": "멤버 관리",
"detail.tab.permissions": "메뉴 권한",
"detail.members.title": "멤버 관리",
"detail.members.description": "이 권한 그룹에 속한 사용자를 관리합니다",
"detail.members.mode.user": "사용자별",
"detail.members.mode.dept": "부서별",
"detail.members.saving": "저장 중...",
"detail.members.save": "멤버 저장",
"detail.members.available": "전체 사용자",
"detail.members.selected": "그룹 멤버",
"detail.members.dept.available": "부서 목록 (선택 시 소속 사용자 전체 추가)",
"detail.members.dept.selected": "추가된 부서",
"detail.members.summary": "현재 그룹 멤버",
"detail.members.summary.count": "현재 그룹 멤버 ({count}명)",
"detail.members.empty": "멤버가 없습니다",
"detail.members.save.success": "멤버가 성공적으로 저장되었습니다.",
"detail.members.save.fail": "멤버 저장에 실패했습니다.",
"detail.members.save.error": "멤버 저장 중 오류가 발생했습니다.",
"detail.permissions.title": "메뉴 권한 설정",
"detail.permissions.description": "이 권한 그룹에서 접근 가능한 메뉴와 권한을 설정합니다",
"detail.permissions.saving": "저장 중...",
"detail.permissions.save": "권한 저장",
"detail.permissions.save.success": "메뉴 권한이 성공적으로 저장되었습니다.",
"detail.permissions.save.fail": "메뉴 권한 저장에 실패했습니다.",
"detail.permissions.save.error": "메뉴 권한 저장 중 오류가 발생했습니다.",
"detail.count.members": "{count}명",
};
/**
* 권한 그룹 상세 페이지
@@ -26,6 +101,7 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
const { user: currentUser } = useAuth();
const router = useRouter();
const { refreshMenus } = useMenu();
const { t } = usePageMultiLang({ keys: LANG_KEYS, defaults: DEFAULT_TEXTS, menuCode: "roles.management" });
const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN";
@@ -61,11 +137,11 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
if (response.success && response.data) {
setRoleGroup(response.data);
} else {
setError(response.message || "권한 그룹 정보를 불러오는데 실패했습니다.");
setError(response.message || t("detail.error.load"));
}
} catch (err) {
console.error("권한 그룹 정보 로드 오류:", err);
setError("권한 그룹 정보를 불러오는 중 오류가 발생했습니다.");
setError(t("detail.error.load.generic"));
} finally {
setIsLoading(false);
}
@@ -130,7 +206,7 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
return {
id: `dept_${deptCode}`,
label: dept.deptName || dept.dept_name || deptCode,
description: `${userIds.length}`,
description: t("detail.count.members", { count: userIds.length }),
userIds,
};
}).filter((d: any) => d.userIds.length > 0),
@@ -236,17 +312,17 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
const response = await roleAPI.updateMembers(roleGroup.objid, selectedUserIds);
if (response.success) {
alert("멤버가 성공적으로 저장되었습니다.");
alert(t("detail.members.save.success"));
loadMembers(); // 새로고침
// 사이드바 메뉴 새로고침 (현재 사용자가 영향받을 수 있음)
await refreshMenus();
} else {
alert(response.message || "멤버 저장에 실패했습니다.");
alert(response.message || t("detail.members.save.fail"));
}
} catch (err) {
console.error("멤버 저장 오류:", err);
alert("멤버 저장 중 오류가 발생했습니다.");
alert(t("detail.members.save.error"));
} finally {
setIsSavingMembers(false);
}
@@ -261,17 +337,17 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
const response = await roleAPI.setMenuPermissions(roleGroup.objid, menuPermissions);
if (response.success) {
alert("메뉴 권한이 성공적으로 저장되었습니다.");
alert(t("detail.permissions.save.success"));
loadMenuPermissions(); // 새로고침
// 사이드바 메뉴 새로고침 (권한 변경 즉시 반영)
await refreshMenus();
} else {
alert(response.message || "메뉴 권한 저장에 실패했습니다.");
alert(response.message || t("detail.permissions.save.fail"));
}
} catch (err) {
console.error("메뉴 권한 저장 오류:", err);
alert("메뉴 권한 저장 중 오류가 발생했습니다.");
alert(t("detail.permissions.save.error"));
} finally {
setIsSavingPermissions(false);
}
@@ -282,7 +358,7 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border p-12 shadow-sm">
<div className="flex flex-col items-center gap-4">
<div className="border-primary h-8 w-8 animate-spin rounded-full border-4 border-t-transparent"></div>
<p className="text-muted-foreground text-sm"> ...</p>
<p className="text-muted-foreground text-sm">{t("detail.loading")}</p>
</div>
</div>
);
@@ -292,10 +368,10 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
return (
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border p-6 shadow-sm">
<AlertCircle className="text-destructive mb-4 h-12 w-12" />
<h3 className="mb-2 text-lg font-semibold"> </h3>
<p className="text-muted-foreground mb-4 text-center text-sm">{error || "권한 그룹을 찾을 수 없습니다."}</p>
<h3 className="mb-2 text-lg font-semibold">{t("detail.error.title")}</h3>
<p className="text-muted-foreground mb-4 text-center text-sm">{error || t("detail.error.notfound")}</p>
<Button variant="outline" onClick={() => router.push("/admin/userMng/rolesList")}>
{t("detail.button.backToList")}
</Button>
</div>
);
@@ -321,7 +397,7 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
roleGroup.status === "active" ? "bg-emerald-100 text-emerald-800" : "bg-muted text-foreground"
}`}
>
{roleGroup.status === "active" ? "활성" : "비활성"}
{roleGroup.status === "active" ? t("detail.status.active") : t("detail.status.inactive")}
</span>
</div>
</div>
@@ -337,7 +413,7 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
}`}
>
<Users className="h-4 w-4" />
({selectedUsers.length})
{t("detail.tab.members")} ({selectedUsers.length})
</button>
<button
onClick={() => setActiveTab("permissions")}
@@ -348,7 +424,7 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
}`}
>
<MenuIcon className="h-4 w-4" />
({menuPermissions.length})
{t("detail.tab.permissions")} ({menuPermissions.length})
</button>
</div>
@@ -358,8 +434,8 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
<>
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold"> </h2>
<p className="text-muted-foreground text-sm"> </p>
<h2 className="text-xl font-semibold">{t("detail.members.title")}</h2>
<p className="text-muted-foreground text-sm">{t("detail.members.description")}</p>
</div>
<div className="flex items-center gap-3">
{/* 사용자별/부서별 모드 전환 */}
@@ -371,7 +447,7 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
}`}
>
<UserIcon className="h-3.5 w-3.5" />
{t("detail.members.mode.user")}
</button>
<button
onClick={() => setMemberMode("dept")}
@@ -380,12 +456,12 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
}`}
>
<Building2 className="h-3.5 w-3.5" />
{t("detail.members.mode.dept")}
</button>
</div>
<Button onClick={handleSaveMembers} disabled={isSavingMembers} className="gap-2">
<Save className="h-4 w-4" />
{isSavingMembers ? "저장 중..." : "멤버 저장"}
{isSavingMembers ? t("detail.members.saving") : t("detail.members.save")}
</Button>
</div>
</div>
@@ -395,8 +471,8 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
availableItems={availableUsers}
selectedItems={selectedUsers}
onSelectionChange={setSelectedUsers}
availableLabel="전체 사용자"
selectedLabel="그룹 멤버"
availableLabel={t("detail.members.available")}
selectedLabel={t("detail.members.selected")}
enableSearch
/>
) : (
@@ -405,8 +481,8 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
availableItems={availableDepts}
selectedItems={selectedDepts}
onSelectionChange={handleDeptSelectionChange}
availableLabel="부서 목록 (선택 시 소속 사용자 전체 추가)"
selectedLabel="추가된 부서"
availableLabel={t("detail.members.dept.available")}
selectedLabel={t("detail.members.dept.selected")}
enableSearch
renderItem={(item) => (
<div className="flex flex-col">
@@ -418,9 +494,9 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
{/* 현재 멤버 요약 */}
<div className="rounded-lg border bg-muted/30 p-4">
<p className="mb-2 text-sm font-semibold"> ({selectedUsers.length})</p>
<p className="mb-2 text-sm font-semibold">{t("detail.members.summary.count", { count: selectedUsers.length })}</p>
{selectedUsers.length === 0 ? (
<p className="text-muted-foreground text-sm"> </p>
<p className="text-muted-foreground text-sm">{t("detail.members.empty")}</p>
) : (
<div className="flex flex-wrap gap-1.5">
{selectedUsers.map((user) => (
@@ -446,12 +522,12 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
<>
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold"> </h2>
<p className="text-muted-foreground text-sm"> </p>
<h2 className="text-xl font-semibold">{t("detail.permissions.title")}</h2>
<p className="text-muted-foreground text-sm">{t("detail.permissions.description")}</p>
</div>
<Button onClick={handleSavePermissions} disabled={isSavingPermissions} className="gap-2">
<Save className="h-4 w-4" />
{isSavingPermissions ? "저장 중..." : "권한 저장"}
{isSavingPermissions ? t("detail.permissions.saving") : t("detail.permissions.save")}
</Button>
</div>

View File

@@ -13,6 +13,63 @@ import { useTabStore } from "@/stores/tabStore";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { companyAPI } from "@/lib/api/company";
import { ScrollToTop } from "@/components/common/ScrollToTop";
import { usePageMultiLang } from "@/hooks/usePageMultiLang";
const LANG_KEYS = [
"roles.title",
"roles.description",
"access.denied",
"access.denied.msg",
"button.back",
"error.occurred",
"error.close.aria",
"roles.list.title",
"filter.company.placeholder",
"filter.company.all",
"button.create",
"loading",
"empty.message",
"empty.hint",
"status.active",
"status.inactive",
"label.company",
"label.memberCount",
"label.menuPermissions",
"count.members",
"count.menus",
"button.edit",
"button.delete",
"error.load.list",
"error.load.list.generic",
] as const;
const DEFAULT_TEXTS: Record<string, string> = {
"roles.title": "권한 그룹 관리",
"roles.description": "회사 내 권한 그룹을 생성하고 멤버를 관리합니다 (회사 관리자 이상)",
"access.denied": "접근 권한 없음",
"access.denied.msg": "권한 그룹 관리는 회사 관리자 이상만 접근할 수 있습니다.",
"button.back": "뒤로 가기",
"error.occurred": "오류가 발생했습니다",
"error.close.aria": "에러 메시지 닫기",
"roles.list.title": "권한 그룹 목록",
"filter.company.placeholder": "회사 선택",
"filter.company.all": "전체 회사",
"button.create": "권한 그룹 생성",
"loading": "권한 그룹 목록을 불러오는 중...",
"empty.message": "등록된 권한 그룹이 없습니다.",
"empty.hint": "권한 그룹을 생성하여 멤버를 관리해보세요.",
"status.active": "활성",
"status.inactive": "비활성",
"label.company": "회사",
"label.memberCount": "멤버 수",
"label.menuPermissions": "메뉴 권한",
"count.members": "{count}명",
"count.menus": "{count}개",
"button.edit": "수정",
"button.delete": "삭제",
"error.load.list": "권한 그룹 목록을 불러오는데 실패했습니다.",
"error.load.list.generic": "권한 그룹 목록을 불러오는 중 오류가 발생했습니다.",
};
/**
* 권한 그룹 관리 페이지
@@ -31,6 +88,7 @@ export default function RolesPage() {
const { user: currentUser } = useAuth();
const router = useRouter();
const openTab = useTabStore((s) => s.openTab);
const { t } = usePageMultiLang({ keys: LANG_KEYS, defaults: DEFAULT_TEXTS, menuCode: "roles.management" });
// 회사 관리자 또는 최고 관리자 여부
const isAdmin =
@@ -95,11 +153,11 @@ export default function RolesPage() {
setRoleGroups(response.data);
console.log("권한 그룹 조회 성공:", response.data.length, "개");
} else {
setError(response.message || "권한 그룹 목록을 불러오는데 실패했습니다.");
setError(response.message || t("error.load.list"));
}
} catch (err) {
console.error("권한 그룹 목록 로드 오류:", err);
setError("권한 그룹 목록을 불러오는 중 오류가 발생했습니다.");
setError(t("error.load.list.generic"));
} finally {
setIsLoading(false);
}
@@ -164,18 +222,18 @@ export default function RolesPage() {
<div className="flex min-h-screen flex-col bg-background">
<div className="space-y-6 p-6">
<div className="space-y-2 border-b pb-4">
<h1 className="text-3xl font-bold tracking-tight"> </h1>
<p className="text-sm text-muted-foreground"> ( )</p>
<h1 className="text-3xl font-bold tracking-tight">{t("roles.title")}</h1>
<p className="text-sm text-muted-foreground">{t("roles.description")}</p>
</div>
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border p-6 shadow-sm">
<AlertCircle className="text-destructive mb-4 h-12 w-12" />
<h3 className="mb-2 text-lg font-semibold"> </h3>
<h3 className="mb-2 text-lg font-semibold">{t("access.denied")}</h3>
<p className="text-muted-foreground mb-4 text-center text-sm">
.
{t("access.denied.msg")}
</p>
<Button variant="outline" onClick={() => window.history.back()}>
{t("button.back")}
</Button>
</div>
</div>
@@ -190,19 +248,19 @@ export default function RolesPage() {
<div className="space-y-6 p-6">
{/* 페이지 헤더 */}
<div className="space-y-2 border-b pb-4">
<h1 className="text-3xl font-bold tracking-tight"> </h1>
<p className="text-sm text-muted-foreground"> ( )</p>
<h1 className="text-3xl font-bold tracking-tight">{t("roles.title")}</h1>
<p className="text-sm text-muted-foreground">{t("roles.description")}</p>
</div>
{/* 에러 메시지 */}
{error && (
<div className="border-destructive/50 bg-destructive/10 rounded-lg border p-4">
<div className="flex items-center justify-between">
<p className="text-destructive text-sm font-semibold"> </p>
<p className="text-destructive text-sm font-semibold">{t("error.occurred")}</p>
<button
onClick={() => setError(null)}
className="text-destructive hover:text-destructive/80 transition-colors"
aria-label="에러 메시지 닫기"
aria-label={t("error.close.aria")}
>
</button>
@@ -214,7 +272,7 @@ export default function RolesPage() {
{/* 액션 버튼 영역 */}
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="flex items-center gap-4">
<h2 className="text-xl font-semibold"> ({roleGroups.length})</h2>
<h2 className="text-xl font-semibold">{t("roles.list.title")} ({roleGroups.length})</h2>
{/* 최고 관리자 전용: 회사 필터 */}
{isSuperAdmin && (
@@ -222,10 +280,10 @@ export default function RolesPage() {
<Filter className="text-muted-foreground h-4 w-4" />
<Select value={selectedCompany} onValueChange={(value) => setSelectedCompany(value)}>
<SelectTrigger className="h-10 w-[200px]">
<SelectValue placeholder="회사 선택" />
<SelectValue placeholder={t("filter.company.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
<SelectItem value="all">{t("filter.company.all")}</SelectItem>
{companies.map((company) => (
<SelectItem key={company.company_code} value={company.company_code}>
{company.company_name}
@@ -244,7 +302,7 @@ export default function RolesPage() {
<Button onClick={handleCreateRole} className="gap-2">
<Plus className="h-4 w-4" />
{t("button.create")}
</Button>
</div>
@@ -253,14 +311,14 @@ export default function RolesPage() {
<div className="bg-card rounded-lg border p-12 shadow-sm">
<div className="flex flex-col items-center justify-center gap-4">
<div className="border-primary h-8 w-8 animate-spin rounded-full border-4 border-t-transparent"></div>
<p className="text-muted-foreground text-sm"> ...</p>
<p className="text-muted-foreground text-sm">{t("loading")}</p>
</div>
</div>
) : roleGroups.length === 0 ? (
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border shadow-sm">
<div className="flex flex-col items-center gap-2 text-center">
<p className="text-muted-foreground text-sm"> .</p>
<p className="text-muted-foreground text-xs"> .</p>
<p className="text-muted-foreground text-sm">{t("empty.message")}</p>
<p className="text-muted-foreground text-xs">{t("empty.hint")}</p>
</div>
</div>
) : (
@@ -282,7 +340,7 @@ export default function RolesPage() {
role.status === "active" ? "bg-emerald-100 text-emerald-800" : "bg-muted text-foreground"
}`}
>
{role.status === "active" ? "활성" : "비활성"}
{role.status === "active" ? t("status.active") : t("status.inactive")}
</span>
</div>
@@ -291,7 +349,7 @@ export default function RolesPage() {
{/* 최고 관리자는 회사명 표시 */}
{isSuperAdmin && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground">{t("label.company")}</span>
<span className="font-medium">
{companies.find((c) => c.company_code === role.companyCode)?.company_name || role.companyCode}
</span>
@@ -300,16 +358,16 @@ export default function RolesPage() {
<div className="flex justify-between text-sm">
<span className="text-muted-foreground flex items-center gap-1">
<Users className="h-3 w-3" />
{t("label.memberCount")}
</span>
<span className="font-medium">{role.memberCount || 0}</span>
<span className="font-medium">{t("count.members", { count: role.memberCount || 0 })}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground flex items-center gap-1">
<Menu className="h-3 w-3" />
{t("label.menuPermissions")}
</span>
<span className="font-medium">{role.menuCount || 0}</span>
<span className="font-medium">{t("count.menus", { count: role.menuCount || 0 })}</span>
</div>
</div>
</div>
@@ -326,7 +384,7 @@ export default function RolesPage() {
className="flex-1 gap-1 text-xs"
>
<Edit className="h-3 w-3" />
{t("button.edit")}
</Button>
<Button
variant="outline"
@@ -338,7 +396,7 @@ export default function RolesPage() {
className="text-destructive hover:bg-destructive hover:text-destructive-foreground gap-1 text-xs"
>
<Trash2 className="h-3 w-3" />
{t("button.delete")}
</Button>
</div>
</div>

View File

@@ -8,6 +8,46 @@ import { useAuth } from "@/hooks/useAuth";
import { AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrollToTop } from "@/components/common/ScrollToTop";
import { usePageMultiLang } from "@/hooks/usePageMultiLang";
// 다국어 키 목록
const USER_AUTH_KEYS = [
"user.auth.title", "user.auth.description",
"access.denied", "access.denied.msg", "button.back",
"error.occurred", "error.load.users", "error.load.users.detail",
"role.super_admin", "role.company_admin", "role.user", "role.guest", "role.partner", "role.unassigned",
"table.userId", "table.userName", "table.company", "table.dept", "table.currentAuth",
"table.empty", "table.actions", "action.change.auth",
"pagination.prev", "pagination.next",
] as const;
// 한국어 기본 텍스트
const USER_AUTH_DEFAULTS: Record<string, string> = {
"user.auth.title": "사용자 권한 관리",
"user.auth.description": "사용자별 권한 레벨을 관리합니다.",
"access.denied": "접근 권한 없음",
"access.denied.msg": "권한 관리는 관리자만 접근할 수 있습니다.",
"button.back": "뒤로 가기",
"error.occurred": "오류가 발생했습니다",
"error.load.users": "사용자 목록을 불러오는데 실패했습니다.",
"error.load.users.detail": "사용자 목록을 불러오는 중 오류가 발생했습니다.",
"role.super_admin": "최고 관리자",
"role.company_admin": "회사 관리자",
"role.user": "일반 사용자",
"role.guest": "게스트",
"role.partner": "협력업체",
"role.unassigned": "미지정",
"table.userId": "사용자 ID",
"table.userName": "사용자명",
"table.company": "회사",
"table.dept": "부서",
"table.currentAuth": "현재 권한",
"table.empty": "등록된 사용자가 없습니다.",
"table.actions": "액션",
"action.change.auth": "권한 변경",
"pagination.prev": "이전",
"pagination.next": "다음",
};
/**
* 사용자 권한 관리 페이지
@@ -18,6 +58,11 @@ import { ScrollToTop } from "@/components/common/ScrollToTop";
* 회사관리자는 SUPER_ADMIN 권한 부여 불가
*/
export default function UserAuthPage() {
const { t } = usePageMultiLang({
keys: USER_AUTH_KEYS,
defaults: USER_AUTH_DEFAULTS,
menuCode: "user.auth",
});
const { user: currentUser } = useAuth();
// 관리자 여부
@@ -65,11 +110,11 @@ export default function UserAuthPage() {
totalPages: Math.ceil((response.total || 0) / (response.pageSize || paginationInfo.pageSize)),
});
} else {
setError(response.message || "사용자 목록을 불러오는데 실패했습니다.");
setError(response.message || t("error.load.users"));
}
} catch (err) {
console.error("사용자 목록 로드 오류:", err);
setError("사용자 목록을 불러오는 중 오류가 발생했습니다.");
setError(t("error.load.users.detail"));
} finally {
setIsLoading(false);
}
@@ -114,18 +159,18 @@ export default function UserAuthPage() {
<div className="bg-background flex min-h-screen flex-col">
<div className="space-y-6 p-6">
<div className="space-y-2 border-b pb-4">
<h1 className="text-3xl font-bold tracking-tight"> </h1>
<p className="text-muted-foreground text-sm"> .</p>
<h1 className="text-3xl font-bold tracking-tight">{t("user.auth.title")}</h1>
<p className="text-muted-foreground text-sm">{t("user.auth.description")}</p>
</div>
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border p-6 shadow-sm">
<AlertCircle className="text-destructive mb-4 h-12 w-12" />
<h3 className="mb-2 text-lg font-semibold"> </h3>
<h3 className="mb-2 text-lg font-semibold">{t("access.denied")}</h3>
<p className="text-muted-foreground mb-4 text-center text-sm">
.
{t("access.denied.msg")}
</p>
<Button variant="outline" onClick={() => window.history.back()}>
{t("button.back")}
</Button>
</div>
</div>
@@ -140,15 +185,15 @@ export default function UserAuthPage() {
<div className="space-y-6 p-6">
{/* 페이지 헤더 */}
<div className="space-y-2 border-b pb-4">
<h1 className="text-3xl font-bold tracking-tight"> </h1>
<p className="text-muted-foreground text-sm"> .</p>
<h1 className="text-3xl font-bold tracking-tight">{t("user.auth.title")}</h1>
<p className="text-muted-foreground text-sm">{t("user.auth.description")}</p>
</div>
{/* 에러 메시지 */}
{error && (
<div className="border-destructive/50 bg-destructive/10 rounded-lg border p-4">
<div className="flex items-center justify-between">
<p className="text-destructive text-sm font-semibold"> </p>
<p className="text-destructive text-sm font-semibold">{t("error.occurred")}</p>
<button
onClick={() => setError(null)}
className="text-destructive hover:text-destructive/80 transition-colors"
@@ -169,6 +214,7 @@ export default function UserAuthPage() {
paginationInfo={paginationInfo}
onEditAuth={handleEditAuth}
onPageChange={handlePageChange}
t={t}
/>
{/* 권한 변경 모달 */}

View File

@@ -8,6 +8,90 @@ import { Pagination } from "@/components/common/Pagination";
import { UserPasswordResetModal } from "@/components/admin/UserPasswordResetModal";
import { UserFormModal } from "@/components/admin/UserFormModal";
import { ScrollToTop } from "@/components/common/ScrollToTop";
import { usePageMultiLang } from "@/hooks/usePageMultiLang";
// 다국어 키 목록
const USER_MNG_KEYS = [
"user.mng.title",
"user.mng.description",
"error.occurred",
"table.sabun",
"table.company",
"table.dept",
"table.position",
"table.userId",
"table.userName",
"table.phone",
"table.email",
"table.regDate",
"table.status",
"table.dept.short",
"table.contact",
"table.empty",
"table.actions",
"action.edit.user",
"action.reset.password",
"action.view.history",
"toolbar.search.placeholder",
"toolbar.searching",
"toolbar.advanced.search",
"toolbar.advanced.search.title",
"toolbar.advanced.search.desc",
"toolbar.advanced.mode.warning",
"toolbar.advanced.reset",
"toolbar.total.count",
"toolbar.total.unit",
"toolbar.create.user",
"toolbar.search.company",
"toolbar.search.dept",
"toolbar.search.position",
"toolbar.search.userId",
"toolbar.search.userName",
"toolbar.search.tel",
"toolbar.search.email",
] as const;
// 한국어 기본값
const USER_MNG_DEFAULTS: Record<string, string> = {
"user.mng.title": "사용자 관리",
"user.mng.description": "시스템 사용자 계정 및 권한을 관리합니다",
"error.occurred": "오류가 발생했습니다",
"table.sabun": "사번",
"table.company": "회사",
"table.dept": "부서명",
"table.position": "직책",
"table.userId": "사용자 ID",
"table.userName": "사용자명",
"table.phone": "전화번호",
"table.email": "이메일",
"table.regDate": "등록일",
"table.status": "상태",
"table.dept.short": "부서",
"table.contact": "연락처",
"table.empty": "등록된 사용자가 없습니다.",
"table.actions": "작업",
"action.edit.user": "사용자 정보 수정",
"action.reset.password": "비밀번호 초기화",
"action.view.history": "변경이력 조회",
"toolbar.search.placeholder": "통합 검색...",
"toolbar.searching": "검색 중...",
"toolbar.advanced.search": "고급 검색",
"toolbar.advanced.search.title": "고급 검색 옵션",
"toolbar.advanced.search.desc": "각 필드별로 개별 검색 조건을 설정할 수 있습니다",
"toolbar.advanced.mode.warning":
"고급 검색 모드가 활성화되어 있습니다. 통합 검색을 사용하려면 고급 검색 조건을 초기화하세요.",
"toolbar.advanced.reset": "고급 검색 조건 초기화",
"toolbar.total.count": "총",
"toolbar.total.unit": "명",
"toolbar.create.user": "사용자 등록",
"toolbar.search.company": "회사명 검색",
"toolbar.search.dept": "부서명 검색",
"toolbar.search.position": "직책 검색",
"toolbar.search.userId": "사용자 ID 검색",
"toolbar.search.userName": "사용자명 검색",
"toolbar.search.tel": "전화번호/휴대폰 검색",
"toolbar.search.email": "이메일 검색",
};
/**
* 사용자관리 페이지
@@ -18,6 +102,11 @@ import { ScrollToTop } from "@/components/common/ScrollToTop";
* - 실제 데이터베이스와 연동되어 작동
*/
export default function UserMngPage() {
const { t } = usePageMultiLang({
keys: USER_MNG_KEYS,
defaults: USER_MNG_DEFAULTS,
menuCode: "user.management",
});
const {
// 데이터
users,
@@ -113,8 +202,8 @@ export default function UserMngPage() {
{/* 상단 고정: 헤더 + 툴바 */}
<div className="shrink-0 space-y-6 p-6 pb-0">
<div className="space-y-2 border-b pb-4">
<h1 className="text-3xl font-bold tracking-tight"> </h1>
<p className="text-sm text-muted-foreground"> </p>
<h1 className="text-3xl font-bold tracking-tight">{t("user.mng.title")}</h1>
<p className="text-sm text-muted-foreground">{t("user.mng.description")}</p>
</div>
<UserToolbar
@@ -123,12 +212,13 @@ export default function UserMngPage() {
isSearching={isSearching}
onSearchChange={updateSearchFilter}
onCreateClick={handleCreateUser}
t={t}
/>
{error && (
<div className="border-destructive/50 bg-destructive/10 rounded-lg border p-4">
<div className="flex items-center justify-between">
<p className="text-destructive text-sm font-semibold"> </p>
<p className="text-destructive text-sm font-semibold">{t("error.occurred")}</p>
<button
onClick={clearError}
className="text-destructive hover:text-destructive/80 transition-colors"
@@ -151,6 +241,7 @@ export default function UserMngPage() {
onStatusToggle={handleStatusToggle}
onPasswordReset={handlePasswordReset}
onEdit={handleEditUser}
t={t}
/>
</div>