diff --git a/frontend/app/(main)/admin/systemMng/i18nList/page.tsx b/frontend/app/(main)/admin/systemMng/i18nList/page.tsx index 0ce088fb..56e3da71 100644 --- a/frontend/app/(main)/admin/systemMng/i18nList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/i18nList/page.tsx @@ -651,15 +651,17 @@ export default function I18nPage() { } return ( -
-
-
+
+
+
{/* 탭 네비게이션 */}
{/* 메인 콘텐츠 영역 */} -
+
{/* 언어 관리 탭 */} {activeTab === "languages" && ( - + 언어 관리 - +
총 {languages.length}개의 언어가 등록되어 있습니다.
@@ -701,16 +705,16 @@ export default function I18nPage() { {/* 다국어 키 관리 탭 */} {activeTab === "keys" && ( -
+
{/* 좌측: 카테고리 트리 (2/12) */} - +
카테고리
- - + + setSelectedCategory(cat)} @@ -724,7 +728,7 @@ export default function I18nPage() {
{/* 중앙: 언어 키 목록 (6/12) */} - +
@@ -758,7 +762,7 @@ export default function I18nPage() {
- + {/* 검색 필터 영역 */}
@@ -806,7 +810,7 @@ export default function I18nPage() { {/* 우측: 선택된 키의 다국어 관리 (4/12) */} - + {selectedKey ? ( @@ -821,11 +825,11 @@ export default function I18nPage() { )} - + {selectedKey ? ( -
+
{/* 스크롤 가능한 텍스트 영역 */} -
+
{languages .filter((lang) => lang.isActive === "Y") .map((lang) => { @@ -854,7 +858,7 @@ export default function I18nPage() {
) : ( -
+
언어 키를 선택하세요
좌측 목록에서 편집할 언어 키를 클릭하세요
diff --git a/frontend/app/(main)/admin/userMng/companyList/page.tsx b/frontend/app/(main)/admin/userMng/companyList/page.tsx index 1f08f097..c3a2f4bd 100644 --- a/frontend/app/(main)/admin/userMng/companyList/page.tsx +++ b/frontend/app/(main)/admin/userMng/companyList/page.tsx @@ -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 = { + "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() {
-

회사 관리

-

시스템에서 사용하는 회사 정보를 관리합니다

+

{t("company.page.title")}

+

{t("company.page.description")}

-

접근 권한 없음

+

{t("company.access.denied.title")}

- 회사 관리는 최고 관리자만 접근할 수 있습니다. + {t("company.access.denied.description")}

@@ -85,8 +194,8 @@ export default function CompanyPage() {
{/* 페이지 헤더 */}
-

회사 관리

-

시스템에서 사용하는 회사 정보를 관리합니다

+

{t("company.page.title")}

+

{t("company.page.description")}

{/* 디스크 사용량 요약 */} @@ -100,10 +209,11 @@ export default function CompanyPage() { onSearchChange={updateSearchFilter} onSearchClear={clearSearchFilter} onCreateClick={openCreateModal} + t={t} /> {/* 회사 목록 테이블 */} - + {/* 회사 등록/수정 모달 */} {/* 회사 삭제 확인 다이얼로그 */} @@ -124,6 +235,7 @@ export default function CompanyPage() { onClose={closeDeleteDialog} onConfirm={deleteCompany} onClearError={clearError} + t={t} />
diff --git a/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx b/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx index f78b4a37..9ba74114 100644 --- a/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx +++ b/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx @@ -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 = { + "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
-

권한 그룹 정보를 불러오는 중...

+

{t("detail.loading")}

); @@ -292,10 +368,10 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin return (
-

오류 발생

-

{error || "권한 그룹을 찾을 수 없습니다."}

+

{t("detail.error.title")}

+

{error || t("detail.error.notfound")}

); @@ -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")}
@@ -337,7 +413,7 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin }`} > - 멤버 관리 ({selectedUsers.length}) + {t("detail.tab.members")} ({selectedUsers.length})
@@ -358,8 +434,8 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin <>
-

멤버 관리

-

이 권한 그룹에 속한 사용자를 관리합니다

+

{t("detail.members.title")}

+

{t("detail.members.description")}

{/* 사용자별/부서별 모드 전환 */} @@ -371,7 +447,7 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin }`} > - 사용자별 + {t("detail.members.mode.user")}
@@ -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) => (
@@ -418,9 +494,9 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin {/* 현재 멤버 요약 */}
-

현재 그룹 멤버 ({selectedUsers.length}명)

+

{t("detail.members.summary.count", { count: selectedUsers.length })}

{selectedUsers.length === 0 ? ( -

멤버가 없습니다

+

{t("detail.members.empty")}

) : (
{selectedUsers.map((user) => ( @@ -446,12 +522,12 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin <>
-

메뉴 권한 설정

-

이 권한 그룹에서 접근 가능한 메뉴와 권한을 설정합니다

+

{t("detail.permissions.title")}

+

{t("detail.permissions.description")}

diff --git a/frontend/app/(main)/admin/userMng/rolesList/page.tsx b/frontend/app/(main)/admin/userMng/rolesList/page.tsx index 58ba5359..2bdacd2d 100644 --- a/frontend/app/(main)/admin/userMng/rolesList/page.tsx +++ b/frontend/app/(main)/admin/userMng/rolesList/page.tsx @@ -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 = { + "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() {
-

권한 그룹 관리

-

회사 내 권한 그룹을 생성하고 멤버를 관리합니다 (회사 관리자 이상)

+

{t("roles.title")}

+

{t("roles.description")}

-

접근 권한 없음

+

{t("access.denied")}

- 권한 그룹 관리는 회사 관리자 이상만 접근할 수 있습니다. + {t("access.denied.msg")}

@@ -190,19 +248,19 @@ export default function RolesPage() {
{/* 페이지 헤더 */}
-

권한 그룹 관리

-

회사 내 권한 그룹을 생성하고 멤버를 관리합니다 (회사 관리자 이상)

+

{t("roles.title")}

+

{t("roles.description")}

{/* 에러 메시지 */} {error && (
-

오류가 발생했습니다

+

{t("error.occurred")}

@@ -214,7 +272,7 @@ export default function RolesPage() { {/* 액션 버튼 영역 */}
-

권한 그룹 목록 ({roleGroups.length})

+

{t("roles.list.title")} ({roleGroups.length})

{/* 최고 관리자 전용: 회사 필터 */} {isSuperAdmin && ( @@ -222,10 +280,10 @@ export default function RolesPage() { onFormChange("company_name", e.target.value)} - placeholder="회사명을 입력하세요" + placeholder={_t("company.form.companyNamePlaceholder")} disabled={isLoading || isSaving} className={error ? "border-destructive" : ""} autoFocus @@ -148,7 +174,7 @@ export function CompanyFormModal({ {/* 사업자등록번호 입력 (필수) */}
{businessNumberError}

) : ( -

10자리 숫자 (자동 하이픈 추가)

+

{_t("company.form.businessNumberHint")}

)}
{/* 대표자명 입력 */}
- + onFormChange("representative_name", e.target.value)} - placeholder="대표자명을 입력하세요" + placeholder={_t("company.form.representativeNamePlaceholder")} disabled={isLoading || isSaving} />
{/* 대표 연락처 입력 */}
- + - + - + - + onFormChange("address", e.target.value)} - placeholder="서울특별시 강남구..." + placeholder={_t("company.form.addressPlaceholder")} disabled={isLoading || isSaving} />
@@ -241,13 +267,13 @@ export function CompanyFormModal({

- 회사 코드: {modalState.selectedCompany.company_code} + {_t("company.form.companyCodeLabel")} {modalState.selectedCompany.company_code}

- 등록자: {modalState.selectedCompany.writer} + {_t("company.form.writerLabel")} {modalState.selectedCompany.writer}

- 등록일:{" "} + {_t("company.form.regdateLabel")}{" "} {new Date(modalState.selectedCompany.regdate).toLocaleDateString("ko-KR")}

@@ -257,7 +283,7 @@ export function CompanyFormModal({ diff --git a/frontend/components/admin/CompanyTable.tsx b/frontend/components/admin/CompanyTable.tsx index 0855fef9..7e72898c 100644 --- a/frontend/components/admin/CompanyTable.tsx +++ b/frontend/components/admin/CompanyTable.tsx @@ -9,12 +9,37 @@ interface CompanyTableProps { isLoading: boolean; onEdit: (company: Company) => void; onDelete: (company: Company) => void; + t?: (key: string, params?: Record) => string; } /** * 회사 목록 테이블 컴포넌트 */ -export function CompanyTable({ companies, isLoading, onEdit, onDelete }: CompanyTableProps) { +export function CompanyTable({ companies, isLoading, onEdit, onDelete, t }: CompanyTableProps) { + const _t = t || ((key: string, params?: Record) => { + const defaults: Record = { + "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": "삭제", + }; + let text = defaults[key] || key; + if (params) { + Object.entries(params).forEach(([k, v]) => { + text = text.replace(`{${k}}`, String(v)); + }); + } + return text; + }); const router = useRouter(); // 부서 관리 페이지로 이동 @@ -28,7 +53,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company return (
- 정보 없음 + {_t("company.table.diskNoInfo")}
); } @@ -39,7 +64,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company
- {fileCount}개 파일 + {_t("company.table.fileCount", { count: fileCount })}
@@ -53,23 +78,23 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company const columns: RDVColumn[] = [ { key: "company_code", - label: "회사코드", + label: _t("company.table.companyCode"), width: "12%", render: (value) => {value}, }, { key: "company_name", - label: "회사명", + label: _t("company.table.companyName"), render: (value) => {value}, }, { key: "writer", - label: "등록자", + label: _t("company.table.writer"), width: "15%", }, { key: "diskUsage", - label: "디스크 사용량", + label: _t("company.table.diskUsage"), width: "15%", hideOnMobile: true, render: (_value, row) => formatDiskUsage(row), @@ -79,11 +104,11 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company // 모바일 카드 필드 정의 const cardFields: RDVCardField[] = [ { - label: "작성자", + label: _t("company.table.cardWriter"), render: (company) => {company.writer}, }, { - label: "디스크 사용량", + label: _t("company.table.cardDiskUsage"), render: (company) => formatDiskUsage(company), }, ]; @@ -94,12 +119,12 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company columns={columns} keyExtractor={(c) => c.regdate + c.company_code} isLoading={isLoading} - emptyMessage="등록된 회사가 없습니다." + emptyMessage={_t("company.table.empty")} skeletonCount={10} cardTitle={(c) => c.company_name} cardSubtitle={(c) => {c.company_code}} cardFields={cardFields} - actionsLabel="작업" + actionsLabel={_t("company.table.actions")} actionsWidth="12%" tableContainerClassName="!block" cardContainerClassName="!hidden" @@ -110,7 +135,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company size="icon" onClick={() => handleManageDepartments(company)} className="h-8 w-8" - aria-label="부서관리" + aria-label={_t("company.table.actionDept")} > @@ -119,7 +144,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company size="icon" onClick={() => onEdit(company)} className="h-8 w-8" - aria-label="수정" + aria-label={_t("company.table.actionEdit")} > @@ -128,7 +153,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company size="icon" onClick={() => onDelete(company)} className="text-destructive hover:bg-destructive/10 hover:text-destructive h-8 w-8" - aria-label="삭제" + aria-label={_t("company.table.actionDelete")} > diff --git a/frontend/components/admin/CompanyToolbar.tsx b/frontend/components/admin/CompanyToolbar.tsx index 8eb28f01..a399a5e1 100644 --- a/frontend/components/admin/CompanyToolbar.tsx +++ b/frontend/components/admin/CompanyToolbar.tsx @@ -9,24 +9,33 @@ interface CompanyToolbarProps { onSearchChange: (filter: Partial) => void; onSearchClear: () => void; onCreateClick: () => void; + t?: (key: string, params?: Record) => string; } /** * 회사 관리 툴바 컴포넌트 * 검색, 필터링, 등록 기능 제공 */ -export function CompanyToolbar({ totalCount, onCreateClick }: CompanyToolbarProps) { +export function CompanyToolbar({ totalCount, onCreateClick, t }: CompanyToolbarProps) { + const _t = t || ((key: string) => { + const defaults: Record = { + "company.toolbar.total": "총", + "company.toolbar.create": "회사 등록", + }; + return defaults[key] || key; + }); + return (
{/* 왼쪽: 카운트 정보 */}
- 총 {totalCount.toLocaleString()} 건 + {_t("company.toolbar.total")} {totalCount.toLocaleString()}
{/* 오른쪽: 등록 버튼 */}
); diff --git a/frontend/components/admin/UserAuthTable.tsx b/frontend/components/admin/UserAuthTable.tsx index 597afcc7..2ec96eb4 100644 --- a/frontend/components/admin/UserAuthTable.tsx +++ b/frontend/components/admin/UserAuthTable.tsx @@ -6,6 +6,26 @@ import { Badge } from "@/components/ui/badge"; import { Shield, ShieldCheck, User as UserIcon, Users, Building2 } from "lucide-react"; import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common/ResponsiveDataView"; +// 컴포넌트 내부 기본 텍스트 (t prop이 없을 때 사용) +const USER_AUTH_TABLE_DEFAULTS: Record = { + "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": "다음", +}; + interface UserAuthTableProps { users: any[]; isLoading: boolean; @@ -18,6 +38,7 @@ interface UserAuthTableProps { }; onEditAuth: (user: any) => void; onPageChange: (page: number) => void; + t?: (key: string, params?: Record) => string; } /** @@ -25,43 +46,46 @@ interface UserAuthTableProps { * * 사용자 목록과 권한 정보를 표시하고 권한 변경 기능 제공 */ -export function UserAuthTable({ users, isLoading, isSuperAdmin, paginationInfo, onEditAuth, onPageChange }: UserAuthTableProps) { +export function UserAuthTable({ users, isLoading, isSuperAdmin, paginationInfo, onEditAuth, onPageChange, t }: UserAuthTableProps) { + // 다국어 래퍼 (t prop이 없으면 기본 텍스트 사용) + const _t = t || ((key: string) => USER_AUTH_TABLE_DEFAULTS[key] || key); + // 권한 레벨 표시 const getUserTypeInfo = (userType: string) => { switch (userType) { case "SUPER_ADMIN": return { - label: "최고 관리자", + label: _t("role.super_admin"), icon: , className: "bg-primary/20 text-primary border-primary/30", }; case "COMPANY_ADMIN": return { - label: "회사 관리자", + label: _t("role.company_admin"), icon: , className: "bg-primary/20 text-primary border-primary/30", }; case "USER": return { - label: "일반 사용자", + label: _t("role.user"), icon: , className: "bg-muted/50 text-muted-foreground border-border", }; case "GUEST": return { - label: "게스트", + label: _t("role.guest"), icon: , className: "bg-success/20 text-success border-success/30", }; case "PARTNER": return { - label: "협력업체", + label: _t("role.partner"), icon: , className: "bg-warning/20 text-warning border-warning/30", }; default: return { - label: userType || "미지정", + label: userType || _t("role.unassigned"), icon: , className: "bg-muted/50 text-muted-foreground border-border", }; @@ -84,18 +108,18 @@ export function UserAuthTable({ users, isLoading, isSuperAdmin, paginationInfo, }, { key: "userId", - label: "사용자 ID", + label: _t("table.userId"), render: (value) => {value}, }, { key: "userName", - label: "사용자명", + label: _t("table.userName"), }, ...(isSuperAdmin ? [ { key: "companyName", - label: "회사", + label: _t("table.company"), hideOnMobile: true, render: (_value: any, row: any) => {row.companyName || row.companyCode}, } as RDVColumn, @@ -103,13 +127,13 @@ export function UserAuthTable({ users, isLoading, isSuperAdmin, paginationInfo, : []), { key: "deptName", - label: "부서", + label: _t("table.dept"), hideOnMobile: true, render: (value) => {value || "-"}, }, { key: "userType", - label: "현재 권한", + label: _t("table.currentAuth"), className: "text-center", render: (_value, row) => { const typeInfo = getUserTypeInfo(row.userType); @@ -128,13 +152,13 @@ export function UserAuthTable({ users, isLoading, isSuperAdmin, paginationInfo, ...(isSuperAdmin ? [ { - label: "회사", + label: _t("table.company"), render: (user: any) => {user.companyName || user.companyCode}, } as RDVCardField, ] : []), { - label: "부서", + label: _t("table.dept"), render: (user) => {user.deptName || "-"}, }, ]; @@ -146,7 +170,7 @@ export function UserAuthTable({ users, isLoading, isSuperAdmin, paginationInfo, columns={columns} keyExtractor={(u) => u.userId} isLoading={isLoading} - emptyMessage="등록된 사용자가 없습니다." + emptyMessage={_t("table.empty")} skeletonCount={10} cardTitle={(u) => u.userName} cardSubtitle={(u) => {u.userId}} @@ -160,12 +184,12 @@ export function UserAuthTable({ users, isLoading, isSuperAdmin, paginationInfo, ); }} cardFields={cardFields} - actionsLabel="액션" + actionsLabel={_t("table.actions")} actionsWidth="120px" renderActions={(user) => ( )} /> @@ -179,7 +203,7 @@ export function UserAuthTable({ users, isLoading, isSuperAdmin, paginationInfo, onClick={() => onPageChange(paginationInfo.currentPage - 1)} disabled={paginationInfo.currentPage === 1} > - 이전 + {_t("pagination.prev")} {paginationInfo.currentPage} / {paginationInfo.totalPages} @@ -190,7 +214,7 @@ export function UserAuthTable({ users, isLoading, isSuperAdmin, paginationInfo, onClick={() => onPageChange(paginationInfo.currentPage + 1)} disabled={paginationInfo.currentPage === paginationInfo.totalPages} > - 다음 + {_t("pagination.next")}
)} diff --git a/frontend/components/admin/UserTable.tsx b/frontend/components/admin/UserTable.tsx index 84946f85..6f229cc2 100644 --- a/frontend/components/admin/UserTable.tsx +++ b/frontend/components/admin/UserTable.tsx @@ -16,6 +16,7 @@ interface UserTableProps { onStatusToggle: (user: User, newStatus: string) => void; onPasswordReset: (userId: string, userName: string) => void; onEdit: (user: User) => void; + t?: (key: string, params?: Record) => string; } /** @@ -28,7 +29,31 @@ export function UserTable({ onStatusToggle, onPasswordReset, onEdit, + t: tProp, }: UserTableProps) { + // 다국어 함수 (prop이 없으면 한국어 기본값 사용) + const _t = tProp || ((key: string) => { + const defaults: Record = { + "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": "변경이력 조회", + }; + return defaults[key] || key; + }); const { user: currentUser } = useAuth(); const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN"; @@ -118,7 +143,7 @@ export function UserTable({ }, { key: "sabun", - label: "사번", + label: _t("table.sabun"), width: "80px", hideOnMobile: true, render: (value) => {value || "-"}, @@ -127,7 +152,7 @@ export function UserTable({ ? [ { key: "companyCode" as keyof User, - label: "회사", + label: _t("table.company"), width: "120px", hideOnMobile: true, render: (value: any, user: User) => ( @@ -138,42 +163,42 @@ export function UserTable({ : []), { key: "deptName", - label: "부서명", + label: _t("table.dept"), width: "120px", hideOnMobile: true, render: (value) => {value || "-"}, }, { key: "positionName", - label: "직책", + label: _t("table.position"), width: "100px", hideOnMobile: true, render: (value) => {value || "-"}, }, { key: "userId", - label: "사용자 ID", + label: _t("table.userId"), width: "120px", hideOnMobile: true, render: (value) => {value}, }, { key: "userName", - label: "사용자명", + label: _t("table.userName"), width: "100px", hideOnMobile: true, render: (value) => {value}, }, { key: "tel", - label: "전화번호", + label: _t("table.phone"), width: "120px", hideOnMobile: true, render: (_value, row) => {row.tel || row.cellPhone || "-"}, }, { key: "email", - label: "이메일", + label: _t("table.email"), width: "200px", hideOnMobile: true, className: "max-w-[200px] truncate", @@ -183,14 +208,14 @@ export function UserTable({ }, { key: "regDate", - label: "등록일", + label: _t("table.regDate"), width: "100px", hideOnMobile: true, render: (value) => {formatDate(value || "")}, }, { key: "status", - label: "상태", + label: _t("table.status"), width: "120px", hideOnMobile: true, render: (_value, row) => ( @@ -208,14 +233,14 @@ export function UserTable({ // 모바일 카드 필드 정의 const cardFields: RDVCardField[] = [ { - label: "사번", + label: _t("table.sabun"), render: (user) => {user.sabun || "-"}, hideEmpty: true, }, ...(isSuperAdmin ? [ { - label: "회사", + label: _t("table.company"), render: (user: User) => ( {(user as any).companyName || user.companyCode || ""} ), @@ -224,27 +249,27 @@ export function UserTable({ ] : []), { - label: "부서", + label: _t("table.dept.short"), render: (user) => {user.deptName || ""}, hideEmpty: true, }, { - label: "직책", + label: _t("table.position"), render: (user) => {user.positionName || ""}, hideEmpty: true, }, { - label: "연락처", + label: _t("table.contact"), render: (user) => {user.tel || user.cellPhone || ""}, hideEmpty: true, }, { - label: "이메일", + label: _t("table.email"), render: (user) => {user.email || ""}, hideEmpty: true, }, { - label: "등록일", + label: _t("table.regDate"), render: (user) => {formatDate(user.regDate || "")}, }, ]; @@ -256,7 +281,7 @@ export function UserTable({ columns={columns} keyExtractor={(u) => u.userId} isLoading={isLoading} - emptyMessage="등록된 사용자가 없습니다." + emptyMessage={_t("table.empty")} skeletonCount={10} cardTitle={(u) => u.userName || ""} cardSubtitle={(u) => {u.userId}} @@ -268,7 +293,7 @@ export function UserTable({ /> )} cardFields={cardFields} - actionsLabel="작업" + actionsLabel={_t("table.actions")} actionsWidth="200px" renderActions={(user) => ( <> @@ -277,7 +302,7 @@ export function UserTable({ size="icon" onClick={() => onEdit(user)} className="h-8 w-8" - title="사용자 정보 수정" + title={_t("action.edit.user")} > @@ -286,7 +311,7 @@ export function UserTable({ size="icon" onClick={() => onPasswordReset(user.userId, user.userName || user.userId)} className="h-8 w-8" - title="비밀번호 초기화" + title={_t("action.reset.password")} > @@ -295,7 +320,7 @@ export function UserTable({ size="icon" onClick={() => handleOpenHistoryModal(user)} className="h-8 w-8" - title="변경이력 조회" + title={_t("action.view.history")} > diff --git a/frontend/components/admin/UserToolbar.tsx b/frontend/components/admin/UserToolbar.tsx index 927bcfbe..88d5f216 100644 --- a/frontend/components/admin/UserToolbar.tsx +++ b/frontend/components/admin/UserToolbar.tsx @@ -10,6 +10,7 @@ interface UserToolbarProps { isSearching?: boolean; onSearchChange: (searchFilter: Partial) => void; onCreateClick: () => void; + t?: (key: string, params?: Record) => string; } /** @@ -22,7 +23,32 @@ export function UserToolbar({ isSearching = false, onSearchChange, onCreateClick, + t: tProp, }: UserToolbarProps) { + // 다국어 함수 (prop이 없으면 한국어 기본값 사용) + const _t = tProp || ((key: string) => { + const defaults: Record = { + "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": "이메일 검색", + }; + return defaults[key] || key; + }); const [showAdvancedSearch, setShowAdvancedSearch] = useState(false); // 통합 검색어 변경 @@ -77,7 +103,7 @@ export function UserToolbar({ }`} /> handleV2SearchChange(e.target.value)} disabled={isAdvancedSearchMode} @@ -87,10 +113,10 @@ export function UserToolbar({ } ${isAdvancedSearchMode ? "cursor-not-allowed bg-muted text-muted-foreground" : ""}`} />
- {isSearching &&

검색 중...

} + {isSearching &&

{_t("toolbar.searching")}

} {isAdvancedSearchMode && (

- 고급 검색 모드가 활성화되어 있습니다. 통합 검색을 사용하려면 고급 검색 조건을 초기화하세요. + {_t("toolbar.advanced.mode.warning")}

)}
@@ -102,7 +128,7 @@ export function UserToolbar({ onClick={() => setShowAdvancedSearch(!showAdvancedSearch)} className="h-10 gap-2 text-sm font-medium" > - 고급 검색 + {_t("toolbar.advanced.search")} {showAdvancedSearch ? : }
@@ -111,13 +137,13 @@ export function UserToolbar({
{/* 조회 결과 정보 */}
- 총 {totalCount.toLocaleString()} 명 + {_t("toolbar.total.count")} {totalCount.toLocaleString()} {_t("toolbar.total.unit")}
{/* 사용자 등록 버튼 */}
@@ -126,56 +152,56 @@ export function UserToolbar({ {showAdvancedSearch && (
-

고급 검색 옵션

-

각 필드별로 개별 검색 조건을 설정할 수 있습니다

+

{_t("toolbar.advanced.search.title")}

+

{_t("toolbar.advanced.search.desc")}

{/* 고급 검색 필드들 */}
handleAdvancedSearchChange("search_companyName", e.target.value)} className="h-10 text-sm" /> handleAdvancedSearchChange("search_deptName", e.target.value)} className="h-10 text-sm" /> handleAdvancedSearchChange("search_positionName", e.target.value)} className="h-10 text-sm" /> handleAdvancedSearchChange("search_userId", e.target.value)} className="h-10 text-sm" /> handleAdvancedSearchChange("search_userName", e.target.value)} className="h-10 text-sm" /> handleAdvancedSearchChange("search_tel", e.target.value)} className="h-10 text-sm" /> handleAdvancedSearchChange("search_email", e.target.value)} className="h-10 text-sm" @@ -202,7 +228,7 @@ export function UserToolbar({ } className="h-9 text-sm text-muted-foreground hover:text-foreground" > - 고급 검색 조건 초기화 + {_t("toolbar.advanced.reset")}
)} diff --git a/frontend/components/admin/multilang/CategoryTree.tsx b/frontend/components/admin/multilang/CategoryTree.tsx index 9dca27ce..ee99c1ee 100644 --- a/frontend/components/admin/multilang/CategoryTree.tsx +++ b/frontend/components/admin/multilang/CategoryTree.tsx @@ -37,8 +37,8 @@ function CategoryNode({ className={cn( "flex cursor-pointer items-center gap-1 rounded-md px-2 py-1.5 text-sm transition-colors", isSelected - ? "bg-primary text-primary-foreground" - : "hover:bg-muted" + ? "border-l-[3px] border-l-blue-500 bg-blue-50 font-medium text-blue-700 dark:bg-blue-950 dark:text-blue-300" + : "border-l-[3px] border-l-transparent hover:bg-muted" )} style={{ paddingLeft: `${level * 16 + 8}px` }} onClick={() => onSelectCategory(category)} @@ -81,7 +81,7 @@ function CategoryNode({ {category.keyPrefix} @@ -171,8 +171,8 @@ export function CategoryTree({ className={cn( "flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors", selectedCategoryId === null - ? "bg-primary text-primary-foreground" - : "hover:bg-muted" + ? "border-l-[3px] border-l-blue-500 bg-blue-50 font-medium text-blue-700 dark:bg-blue-950 dark:text-blue-300" + : "border-l-[3px] border-l-transparent hover:bg-muted" )} onClick={() => onSelectCategory(null)} > diff --git a/frontend/hooks/usePageMultiLang.ts b/frontend/hooks/usePageMultiLang.ts new file mode 100644 index 00000000..a31daad0 --- /dev/null +++ b/frontend/hooks/usePageMultiLang.ts @@ -0,0 +1,75 @@ +import { useState, useEffect, useCallback } from "react"; +import { apiClient } from "@/lib/api/client"; +import { useMultiLang } from "@/hooks/useMultiLang"; +import { setTranslationCache } from "@/lib/utils/multilang"; + +interface UsePageMultiLangOptions { + keys: readonly string[]; + defaults: Record; + menuCode: string; +} + +/** + * 페이지별 다국어 텍스트 관리 훅 + * - keys: 다국어 키 배열 + * - defaults: 한국어 기본 텍스트 매핑 + * - menuCode: 배치 API에 전달할 메뉴 코드 + */ +export function usePageMultiLang({ keys, defaults, menuCode }: UsePageMultiLangOptions) { + const { userLang } = useMultiLang(); + const [uiTexts, setUiTexts] = useState>(() => ({ ...defaults })); + const [loading, setLoading] = useState(false); + + // 배치 번역 로드 + useEffect(() => { + if (!userLang || loading) return; + + let cancelled = false; + const load = async () => { + setLoading(true); + try { + const response = await apiClient.post( + "/multilang/batch", + { + langKeys: keys, + companyCode: "*", + menuCode, + userLang, + }, + { params: {} }, + ); + + if (!cancelled && response.data.success && response.data.data) { + const merged = { ...defaults, ...response.data.data }; + setUiTexts(merged); + setTranslationCache(userLang, merged); + } + } catch { + // API 실패 시 기본 텍스트 유지 + } finally { + if (!cancelled) setLoading(false); + } + }; + + load(); + return () => { cancelled = true; }; + }, [userLang]); + + // 동기 텍스트 조회 함수 + const t = useCallback( + (key: string, params?: Record): string => { + let text = uiTexts[key] || defaults[key] || key; + + if (params) { + Object.entries(params).forEach(([k, v]) => { + text = text.replace(`{${k}}`, String(v)); + }); + } + + return text; + }, + [uiTexts, defaults], + ); + + return { t, userLang, loading }; +}