diff --git a/frontend/app/(main)/admin/audit-log/page.tsx b/frontend/app/(main)/admin/audit-log/page.tsx index 747d4640..9ee14dcc 100644 --- a/frontend/app/(main)/admin/audit-log/page.tsx +++ b/frontend/app/(main)/admin/audit-log/page.tsx @@ -64,37 +64,252 @@ import { import { getCompanyList } from "@/lib/api/company"; import { useAuth } from "@/hooks/useAuth"; import { Company } from "@/types/company"; +import { usePageMultiLang } from "@/hooks/usePageMultiLang"; + +// 다국어 키 목록 +const LANG_KEYS = [ + "audit.title", + "audit.description", + "audit.button.refresh", + "audit.stats.totalChanges30d", + "audit.stats.resourceTypes", + "audit.stats.activeUsers", + "audit.stats.todayChanges", + "audit.stats.countSuffix", + "audit.stats.typeSuffix", + "audit.stats.userSuffix", + "audit.filter.searchLabel", + "audit.filter.searchPlaceholder", + "audit.filter.typeLabel", + "audit.filter.actionLabel", + "audit.filter.companyLabel", + "audit.filter.userLabel", + "audit.filter.dateFromLabel", + "audit.filter.dateToLabel", + "audit.filter.all", + "audit.filter.allCompanies", + "audit.filter.companySearchPlaceholder", + "audit.filter.companyNotFound", + "audit.filter.userSearchPlaceholder", + "audit.filter.userNotFound", + "audit.filter.apply", + "audit.list.title", + "audit.list.empty", + "audit.list.countSuffix", + "audit.detail.title", + "audit.detail.user", + "audit.detail.company", + "audit.detail.resourceType", + "audit.detail.action", + "audit.detail.resourceName", + "audit.detail.tableName", + "audit.detail.ipAddress", + "audit.detail.summary", + "audit.detail.changes", + "audit.detail.apiPath", + "audit.changes.field", + "audit.changes.before", + "audit.changes.after", + "audit.changes.securityHidden", + "audit.dateGroup.today", + "audit.dateGroup.yesterday", + "audit.fieldValue.empty", + "audit.fieldValue.yes", + "audit.fieldValue.no", + "audit.resource.menu", + "audit.resource.screen", + "audit.resource.screenLayout", + "audit.resource.flow", + "audit.resource.flowStep", + "audit.resource.nodeFlow", + "audit.resource.user", + "audit.resource.role", + "audit.resource.company", + "audit.resource.codeCategory", + "audit.resource.code", + "audit.resource.data", + "audit.resource.table", + "audit.resource.numberingRule", + "audit.action.create", + "audit.action.update", + "audit.action.delete", + "audit.action.copy", + "audit.action.login", + "audit.action.statusChange", + "audit.action.batchCreate", + "audit.action.batchUpdate", + "audit.action.batchDelete", + "audit.field.status", + "audit.field.menuUrl", + "audit.field.menuNameKor", + "audit.field.menuNameEng", + "audit.field.screenName", + "audit.field.tableName", + "audit.field.description", + "audit.field.isActive", + "audit.field.userName", + "audit.field.userId", + "audit.field.deptName", + "audit.field.authName", + "audit.field.authCode", + "audit.field.companyCode", + "audit.field.companyName", + "audit.field.name", + "audit.field.userPassword", + "audit.field.prefix", + "audit.field.ruleName", + "audit.field.stepName", + "audit.field.stepOrder", + "audit.field.sourceScreenId", + "audit.field.targetCompanyCode", + "audit.field.mainScreenName", + "audit.field.screenCode", + "audit.field.menuObjid", + "audit.field.deleteReason", + "audit.field.force", + "audit.field.deletedMenus", + "audit.field.failedMenuIds", + "audit.field.deletedCount", + "audit.field.items", +] as const; + +// 한국어 기본 텍스트 +const DEFAULT_TEXTS: Record = { + "audit.title": "통합 변경 이력", + "audit.description": "시스템 전체 변경 사항을 추적합니다", + "audit.button.refresh": "새로고침", + "audit.stats.totalChanges30d": "최근 30일 총 변경", + "audit.stats.resourceTypes": "리소스 유형", + "audit.stats.activeUsers": "활동 사용자", + "audit.stats.todayChanges": "오늘 변경", + "audit.stats.countSuffix": "건", + "audit.stats.typeSuffix": "종", + "audit.stats.userSuffix": "명", + "audit.filter.searchLabel": "검색어", + "audit.filter.searchPlaceholder": "이름, 요약, 사용자...", + "audit.filter.typeLabel": "유형", + "audit.filter.actionLabel": "동작", + "audit.filter.companyLabel": "회사", + "audit.filter.userLabel": "사용자", + "audit.filter.dateFromLabel": "시작일", + "audit.filter.dateToLabel": "종료일", + "audit.filter.all": "전체", + "audit.filter.allCompanies": "전체 회사", + "audit.filter.companySearchPlaceholder": "회사 검색...", + "audit.filter.companyNotFound": "회사를 찾을 수 없습니다", + "audit.filter.userSearchPlaceholder": "사용자 검색...", + "audit.filter.userNotFound": "사용자를 찾을 수 없습니다", + "audit.filter.apply": "필터 적용", + "audit.list.title": "변경 이력", + "audit.list.empty": "변경 이력이 없습니다", + "audit.list.countSuffix": "건", + "audit.detail.title": "변경 상세 정보", + "audit.detail.user": "사용자", + "audit.detail.company": "회사", + "audit.detail.resourceType": "리소스 유형", + "audit.detail.action": "동작", + "audit.detail.resourceName": "리소스명", + "audit.detail.tableName": "테이블명", + "audit.detail.ipAddress": "IP 주소", + "audit.detail.summary": "요약", + "audit.detail.changes": "변경 내역", + "audit.detail.apiPath": "API 경로", + "audit.changes.field": "항목", + "audit.changes.before": "변경 전", + "audit.changes.after": "변경 후", + "audit.changes.securityHidden": "(보안 항목 - 값 비공개)", + "audit.dateGroup.today": "오늘", + "audit.dateGroup.yesterday": "어제", + "audit.fieldValue.empty": "(없음)", + "audit.fieldValue.yes": "예", + "audit.fieldValue.no": "아니오", + "audit.resource.menu": "메뉴", + "audit.resource.screen": "화면", + "audit.resource.screenLayout": "레이아웃", + "audit.resource.flow": "플로우", + "audit.resource.flowStep": "플로우 스텝", + "audit.resource.nodeFlow": "플로우 제어", + "audit.resource.user": "사용자", + "audit.resource.role": "권한", + "audit.resource.company": "회사", + "audit.resource.codeCategory": "코드 카테고리", + "audit.resource.code": "코드", + "audit.resource.data": "데이터", + "audit.resource.table": "테이블", + "audit.resource.numberingRule": "채번 규칙", + "audit.action.create": "생성", + "audit.action.update": "수정", + "audit.action.delete": "삭제", + "audit.action.copy": "복사", + "audit.action.login": "로그인", + "audit.action.statusChange": "상태변경", + "audit.action.batchCreate": "배치생성", + "audit.action.batchUpdate": "배치수정", + "audit.action.batchDelete": "배치삭제", + "audit.field.status": "상태", + "audit.field.menuUrl": "메뉴 URL", + "audit.field.menuNameKor": "메뉴명", + "audit.field.menuNameEng": "메뉴명(영)", + "audit.field.screenName": "화면명", + "audit.field.tableName": "테이블명", + "audit.field.description": "설명", + "audit.field.isActive": "활성 여부", + "audit.field.userName": "사용자명", + "audit.field.userId": "사용자 ID", + "audit.field.deptName": "부서명", + "audit.field.authName": "권한명", + "audit.field.authCode": "권한코드", + "audit.field.companyCode": "회사코드", + "audit.field.companyName": "회사명", + "audit.field.name": "이름", + "audit.field.userPassword": "비밀번호", + "audit.field.prefix": "접두사", + "audit.field.ruleName": "규칙명", + "audit.field.stepName": "스텝명", + "audit.field.stepOrder": "스텝 순서", + "audit.field.sourceScreenId": "원본 화면 ID", + "audit.field.targetCompanyCode": "대상 회사코드", + "audit.field.mainScreenName": "메인 화면명", + "audit.field.screenCode": "화면코드", + "audit.field.menuObjid": "메뉴 ID", + "audit.field.deleteReason": "삭제 사유", + "audit.field.force": "강제 삭제", + "audit.field.deletedMenus": "삭제된 메뉴", + "audit.field.failedMenuIds": "실패한 메뉴", + "audit.field.deletedCount": "삭제 건수", + "audit.field.items": "항목 수", +}; const RESOURCE_TYPE_CONFIG: Record< string, - { label: string; icon: React.ElementType; color: string } + { langKey: string; icon: React.ElementType; color: string } > = { - MENU: { label: "메뉴", icon: Layout, color: "bg-primary/10 text-primary" }, - SCREEN: { label: "화면", icon: Monitor, color: "bg-purple-100 text-purple-700" }, - SCREEN_LAYOUT: { label: "레이아웃", icon: Monitor, color: "bg-purple-100 text-purple-700" }, - FLOW: { label: "플로우", icon: GitBranch, color: "bg-emerald-100 text-emerald-700" }, - FLOW_STEP: { label: "플로우 스텝", icon: GitBranch, color: "bg-emerald-100 text-emerald-700" }, - NODE_FLOW: { label: "플로우 제어", icon: GitBranch, color: "bg-teal-100 text-teal-700" }, - USER: { label: "사용자", icon: User, color: "bg-amber-100 text-orange-700" }, - ROLE: { label: "권한", icon: Shield, color: "bg-destructive/10 text-destructive" }, - COMPANY: { label: "회사", icon: Building2, color: "bg-indigo-100 text-indigo-700" }, - CODE_CATEGORY: { label: "코드 카테고리", icon: Hash, color: "bg-cyan-100 text-cyan-700" }, - CODE: { label: "코드", icon: Hash, color: "bg-cyan-100 text-cyan-700" }, - DATA: { label: "데이터", icon: Database, color: "bg-muted text-foreground" }, - TABLE: { label: "테이블", icon: Database, color: "bg-muted text-foreground" }, - NUMBERING_RULE: { label: "채번 규칙", icon: FileText, color: "bg-amber-100 text-amber-700" }, + MENU: { langKey: "audit.resource.menu", icon: Layout, color: "bg-primary/10 text-primary" }, + SCREEN: { langKey: "audit.resource.screen", icon: Monitor, color: "bg-purple-100 text-purple-700" }, + SCREEN_LAYOUT: { langKey: "audit.resource.screenLayout", icon: Monitor, color: "bg-purple-100 text-purple-700" }, + FLOW: { langKey: "audit.resource.flow", icon: GitBranch, color: "bg-emerald-100 text-emerald-700" }, + FLOW_STEP: { langKey: "audit.resource.flowStep", icon: GitBranch, color: "bg-emerald-100 text-emerald-700" }, + NODE_FLOW: { langKey: "audit.resource.nodeFlow", icon: GitBranch, color: "bg-teal-100 text-teal-700" }, + USER: { langKey: "audit.resource.user", icon: User, color: "bg-amber-100 text-orange-700" }, + ROLE: { langKey: "audit.resource.role", icon: Shield, color: "bg-destructive/10 text-destructive" }, + COMPANY: { langKey: "audit.resource.company", icon: Building2, color: "bg-indigo-100 text-indigo-700" }, + CODE_CATEGORY: { langKey: "audit.resource.codeCategory", icon: Hash, color: "bg-cyan-100 text-cyan-700" }, + CODE: { langKey: "audit.resource.code", icon: Hash, color: "bg-cyan-100 text-cyan-700" }, + DATA: { langKey: "audit.resource.data", icon: Database, color: "bg-muted text-foreground" }, + TABLE: { langKey: "audit.resource.table", icon: Database, color: "bg-muted text-foreground" }, + NUMBERING_RULE: { langKey: "audit.resource.numberingRule", icon: FileText, color: "bg-amber-100 text-amber-700" }, }; -const ACTION_CONFIG: Record = { - CREATE: { label: "생성", color: "bg-emerald-100 text-emerald-700" }, - UPDATE: { label: "수정", color: "bg-primary/10 text-primary" }, - DELETE: { label: "삭제", color: "bg-destructive/10 text-destructive" }, - COPY: { label: "복사", color: "bg-violet-100 text-violet-700" }, - LOGIN: { label: "로그인", color: "bg-muted text-foreground" }, - STATUS_CHANGE: { label: "상태변경", color: "bg-amber-100 text-amber-700" }, - BATCH_CREATE: { label: "배치생성", color: "bg-emerald-100 text-emerald-700" }, - BATCH_UPDATE: { label: "배치수정", color: "bg-primary/10 text-primary" }, - BATCH_DELETE: { label: "배치삭제", color: "bg-destructive/10 text-destructive" }, +const ACTION_CONFIG: Record = { + CREATE: { langKey: "audit.action.create", color: "bg-emerald-100 text-emerald-700" }, + UPDATE: { langKey: "audit.action.update", color: "bg-primary/10 text-primary" }, + DELETE: { langKey: "audit.action.delete", color: "bg-destructive/10 text-destructive" }, + COPY: { langKey: "audit.action.copy", color: "bg-violet-100 text-violet-700" }, + LOGIN: { langKey: "audit.action.login", color: "bg-muted text-foreground" }, + STATUS_CHANGE: { langKey: "audit.action.statusChange", color: "bg-amber-100 text-amber-700" }, + BATCH_CREATE: { langKey: "audit.action.batchCreate", color: "bg-emerald-100 text-emerald-700" }, + BATCH_UPDATE: { langKey: "audit.action.batchUpdate", color: "bg-primary/10 text-primary" }, + BATCH_DELETE: { langKey: "audit.action.batchDelete", color: "bg-destructive/10 text-destructive" }, }; function formatDateTime(dateStr: string): string { @@ -118,59 +333,59 @@ function formatTime(dateStr: string): string { } const FIELD_NAME_MAP: Record = { - status: "상태", - menuUrl: "메뉴 URL", - menu_url: "메뉴 URL", - menuNameKor: "메뉴명", - menu_name_kor: "메뉴명", - menuNameEng: "메뉴명(영)", - menu_name_eng: "메뉴명(영)", - screenName: "화면명", - screen_name: "화면명", - tableName: "테이블명", - table_name: "테이블명", - description: "설명", - isActive: "활성 여부", - is_active: "활성 여부", - userName: "사용자명", - user_name: "사용자명", - userId: "사용자 ID", - user_id: "사용자 ID", - deptName: "부서명", - dept_name: "부서명", - authName: "권한명", - authCode: "권한코드", - companyCode: "회사코드", - company_code: "회사코드", - company_name: "회사명", - name: "이름", - user_password: "비밀번호", - prefix: "접두사", - ruleName: "규칙명", - stepName: "스텝명", - stepOrder: "스텝 순서", - sourceScreenId: "원본 화면 ID", - targetCompanyCode: "대상 회사코드", - mainScreenName: "메인 화면명", - screenCode: "화면코드", - menuObjid: "메뉴 ID", - deleteReason: "삭제 사유", - force: "강제 삭제", - deletedMenus: "삭제된 메뉴", - failedMenuIds: "실패한 메뉴", - deletedCount: "삭제 건수", - items: "항목 수", + status: "audit.field.status", + menuUrl: "audit.field.menuUrl", + menu_url: "audit.field.menuUrl", + menuNameKor: "audit.field.menuNameKor", + menu_name_kor: "audit.field.menuNameKor", + menuNameEng: "audit.field.menuNameEng", + menu_name_eng: "audit.field.menuNameEng", + screenName: "audit.field.screenName", + screen_name: "audit.field.screenName", + tableName: "audit.field.tableName", + table_name: "audit.field.tableName", + description: "audit.field.description", + isActive: "audit.field.isActive", + is_active: "audit.field.isActive", + userName: "audit.field.userName", + user_name: "audit.field.userName", + userId: "audit.field.userId", + user_id: "audit.field.userId", + deptName: "audit.field.deptName", + dept_name: "audit.field.deptName", + authName: "audit.field.authName", + authCode: "audit.field.authCode", + companyCode: "audit.field.companyCode", + company_code: "audit.field.companyCode", + company_name: "audit.field.companyName", + name: "audit.field.name", + user_password: "audit.field.userPassword", + prefix: "audit.field.prefix", + ruleName: "audit.field.ruleName", + stepName: "audit.field.stepName", + stepOrder: "audit.field.stepOrder", + sourceScreenId: "audit.field.sourceScreenId", + targetCompanyCode: "audit.field.targetCompanyCode", + mainScreenName: "audit.field.mainScreenName", + screenCode: "audit.field.screenCode", + menuObjid: "audit.field.menuObjid", + deleteReason: "audit.field.deleteReason", + force: "audit.field.force", + deletedMenus: "audit.field.deletedMenus", + failedMenuIds: "audit.field.failedMenuIds", + deletedCount: "audit.field.deletedCount", + items: "audit.field.items", }; -function formatFieldValue(value: unknown): string { - if (value === null || value === undefined) return "(없음)"; - if (typeof value === "boolean") return value ? "예" : "아니오"; - if (Array.isArray(value)) return value.length > 0 ? `${value.length}건` : "(없음)"; +function formatFieldValue(value: unknown, t: (key: string, params?: Record) => string): string { + if (value === null || value === undefined) return t("audit.fieldValue.empty"); + if (typeof value === "boolean") return value ? t("audit.fieldValue.yes") : t("audit.fieldValue.no"); + if (Array.isArray(value)) return value.length > 0 ? `${value.length}${t("audit.list.countSuffix")}` : t("audit.fieldValue.empty"); if (typeof value === "object") return JSON.stringify(value); return String(value); } -function renderChanges(changes: Record) { +function renderChanges(changes: Record, t: (key: string, params?: Record) => string) { const before = (changes.before as Record) || {}; const after = (changes.after as Record) || {}; const fields = (changes.fields as string[]) || []; @@ -186,9 +401,9 @@ function renderChanges(changes: Record) { const rows = Array.from(allKeys) .filter((key) => key !== "deletedMenus" && key !== "failedMenuIds") .map((key) => ({ - field: FIELD_NAME_MAP[key] || key, - beforeVal: key in before ? formatFieldValue(before[key]) : null, - afterVal: key in after ? formatFieldValue(after[key]) : null, + field: FIELD_NAME_MAP[key] ? t(FIELD_NAME_MAP[key]) : key, + beforeVal: key in before ? formatFieldValue(before[key], t) : null, + afterVal: key in after ? formatFieldValue(after[key], t) : null, isSensitive: fields.includes(key) && !(key in before) && !(key in after), })); @@ -200,15 +415,15 @@ function renderChanges(changes: Record) { - + {hasBefore && ( )} {hasAfter && ( )} @@ -226,7 +441,7 @@ function renderChanges(changes: Record) { } className="px-3 py-1.5 italic text-amber-600" > - (보안 항목 - 값 비공개) + {t("audit.changes.securityHidden")} ) : ( <> @@ -262,14 +477,14 @@ function renderChanges(changes: Record) { ); } -function formatDateGroup(dateStr: string): string { +function formatDateGroup(dateStr: string, t: (key: string) => string): string { const d = new Date(dateStr); const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); - if (d.toDateString() === today.toDateString()) return "오늘"; - if (d.toDateString() === yesterday.toDateString()) return "어제"; + if (d.toDateString() === today.toDateString()) return t("audit.dateGroup.today"); + if (d.toDateString() === yesterday.toDateString()) return t("audit.dateGroup.yesterday"); return d.toLocaleDateString("ko-KR", { year: "numeric", @@ -292,6 +507,7 @@ function groupByDate(entries: AuditLogEntry[]): Map { export default function AuditLogPage() { const { user } = useAuth(); const isSuperAdmin = user?.companyCode === "*" || user?.company_code === "*"; + const { t } = usePageMultiLang({ keys: LANG_KEYS, defaults: DEFAULT_TEXTS, menuCode: "admin.auditLog" }); const [entries, setEntries] = useState([]); const [total, setTotal] = useState(0); @@ -397,9 +613,9 @@ export default function AuditLogPage() {
-

통합 변경 이력

+

{t("audit.title")}

- 시스템 전체 변경 사항을 추적합니다 + {t("audit.description")}

@@ -420,27 +636,27 @@ export default function AuditLogPage() {
-

최근 30일 총 변경

+

{t("audit.stats.totalChanges30d")}

- {stats.dailyCounts.reduce((s, d) => s + d.count, 0).toLocaleString()}건 + {stats.dailyCounts.reduce((s, d) => s + d.count, 0).toLocaleString()}{t("audit.stats.countSuffix")}

-

리소스 유형

-

{stats.resourceTypeCounts.length}종

+

{t("audit.stats.resourceTypes")}

+

{stats.resourceTypeCounts.length}{t("audit.stats.typeSuffix")}

-

활동 사용자

-

{stats.topUsers.length}명

+

{t("audit.stats.activeUsers")}

+

{stats.topUsers.length}{t("audit.stats.userSuffix")}

-

오늘 변경

+

{t("audit.stats.todayChanges")}

{( stats.dailyCounts.find( @@ -448,7 +664,7 @@ export default function AuditLogPage() { new Date(d.date).toDateString() === new Date().toDateString() )?.count || 0 - ).toLocaleString()}건 + ).toLocaleString()}{t("audit.stats.countSuffix")}

@@ -462,11 +678,11 @@ export default function AuditLogPage() { className="flex flex-col gap-3 sm:flex-wrap sm:flex-row sm:items-end" >
- +
handleFilterChange("search", e.target.value)} className="h-9 pl-8 text-sm" @@ -475,7 +691,7 @@ export default function AuditLogPage() {
- + @@ -508,10 +724,10 @@ export default function AuditLogPage() { - 전체 + {t("audit.filter.all")} {Object.entries(ACTION_CONFIG).map(([key, cfg]) => ( - {cfg.label} + {t(cfg.langKey)} ))} @@ -520,7 +736,7 @@ export default function AuditLogPage() { {isSuperAdmin && (
- + @@ -542,10 +758,10 @@ export default function AuditLogPage() { align="start" > - + - 회사를 찾을 수 없습니다 + {t("audit.filter.companyNotFound")} - 전체 회사 + {t("audit.filter.allCompanies")} {companies.map((company) => ( - + @@ -626,10 +842,10 @@ export default function AuditLogPage() { align="start" > - + - 사용자를 찾을 수 없습니다 + {t("audit.filter.userNotFound")} - 전체 + {t("audit.filter.all")} {auditUsers.map((u) => ( {u.user_name} - {u.user_id} ({u.count}건) + {u.user_id} ({u.count}{t("audit.list.countSuffix")})
@@ -685,7 +901,7 @@ export default function AuditLogPage() {
- +
- + - 필터 적용 + {t("audit.filter.apply")} @@ -716,7 +932,7 @@ export default function AuditLogPage() {
- 변경 이력 ({total.toLocaleString()}건) + {t("audit.list.title")} ({total.toLocaleString()}{t("audit.list.countSuffix")})
) : ( @@ -771,10 +987,10 @@ export default function AuditLogPage() {
- {formatDateGroup(items[0].created_at)} + {formatDateGroup(items[0].created_at, t)} - {items.length}건 + {items.length}{t("audit.list.countSuffix")}
{items.map((entry) => { @@ -805,13 +1021,13 @@ export default function AuditLogPage() { variant="secondary" className={`text-[10px] ${rtConfig.color}`} > - {rtConfig.label} + {t(rtConfig.langKey)} - {actConfig.label} + {t(actConfig.langKey)} {entry.company_code && entry.company_code !== "*" && ( @@ -840,7 +1056,7 @@ export default function AuditLogPage() { - 변경 상세 정보 + {t("audit.detail.title")} {selectedEntry && @@ -852,7 +1068,7 @@ export default function AuditLogPage() {

{selectedEntry.user_name || selectedEntry.user_id} @@ -860,7 +1076,7 @@ export default function AuditLogPage() {

{selectedEntry.company_name || selectedEntry.company_code} @@ -868,24 +1084,26 @@ export default function AuditLogPage() {

- {RESOURCE_TYPE_CONFIG[selectedEntry.resource_type]?.label || - selectedEntry.resource_type} + {RESOURCE_TYPE_CONFIG[selectedEntry.resource_type] + ? t(RESOURCE_TYPE_CONFIG[selectedEntry.resource_type].langKey) + : selectedEntry.resource_type}

- +

- {ACTION_CONFIG[selectedEntry.action]?.label || - selectedEntry.action} + {ACTION_CONFIG[selectedEntry.action] + ? t(ACTION_CONFIG[selectedEntry.action].langKey) + : selectedEntry.action}

{selectedEntry.resource_name && (

{selectedEntry.resource_name}

@@ -893,7 +1111,7 @@ export default function AuditLogPage() { {selectedEntry.table_name && (

{selectedEntry.table_name}

@@ -901,7 +1119,7 @@ export default function AuditLogPage() { {selectedEntry.ip_address && (

{selectedEntry.ip_address}

@@ -910,7 +1128,7 @@ export default function AuditLogPage() { {selectedEntry.summary && (
- +

{selectedEntry.summary}

@@ -920,11 +1138,12 @@ export default function AuditLogPage() { {selectedEntry.changes && (
{renderChanges( - selectedEntry.changes as Record + selectedEntry.changes as Record, + t )}
@@ -933,7 +1152,7 @@ export default function AuditLogPage() { {selectedEntry.request_path && (

{selectedEntry.request_path} diff --git a/frontend/app/(main)/admin/system-notices/page.tsx b/frontend/app/(main)/admin/system-notices/page.tsx index 56fd99aa..e98ccaeb 100644 --- a/frontend/app/(main)/admin/system-notices/page.tsx +++ b/frontend/app/(main)/admin/system-notices/page.tsx @@ -33,11 +33,128 @@ import { deleteSystemNotice, } from "@/lib/api/systemNotice"; import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common/ResponsiveDataView"; +import { usePageMultiLang } from "@/hooks/usePageMultiLang"; -function getPriorityLabel(priority: number): { label: string; variant: "default" | "secondary" | "destructive" | "outline" } { - if (priority >= 3) return { label: "높음", variant: "destructive" }; - if (priority === 2) return { label: "보통", variant: "default" }; - return { label: "낮음", variant: "secondary" }; +// 다국어 키 목록 +const LANG_KEYS = [ + "notice.page.title", + "notice.page.description", + "notice.error.title", + "notice.error.closeLabel", + "notice.error.loadFailed", + "notice.error.saveFailed", + "notice.error.deleteFailed", + "notice.filter.statusPlaceholder", + "notice.filter.all", + "notice.filter.active", + "notice.filter.inactive", + "notice.filter.searchPlaceholder", + "notice.list.total", + "notice.list.countSuffix", + "notice.list.refreshLabel", + "notice.list.empty", + "notice.button.create", + "notice.button.editLabel", + "notice.button.deleteLabel", + "notice.column.title", + "notice.column.status", + "notice.column.priority", + "notice.column.author", + "notice.column.createdAt", + "notice.column.actions", + "notice.card.author", + "notice.card.createdAt", + "notice.status.active", + "notice.status.inactive", + "notice.priority.high", + "notice.priority.medium", + "notice.priority.low", + "notice.form.titleCreate", + "notice.form.titleEdit", + "notice.form.descriptionCreate", + "notice.form.descriptionEdit", + "notice.form.titleLabel", + "notice.form.titlePlaceholder", + "notice.form.contentLabel", + "notice.form.contentPlaceholder", + "notice.form.priorityLabel", + "notice.form.priorityPlaceholder", + "notice.form.activeLabel", + "notice.form.cancel", + "notice.form.save", + "notice.form.saving", + "notice.validate.titleRequired", + "notice.validate.contentRequired", + "notice.delete.title", + "notice.delete.description", + "notice.delete.descriptionIrreversible", + "notice.delete.cancel", + "notice.delete.confirm", + "notice.delete.deleting", +] as const; + +// 한국어 기본 텍스트 +const DEFAULT_TEXTS: Record = { + "notice.page.title": "시스템 공지사항", + "notice.page.description": "시스템 사용자에게 전달할 공지사항을 관리합니다.", + "notice.error.title": "오류가 발생했습니다", + "notice.error.closeLabel": "에러 메시지 닫기", + "notice.error.loadFailed": "공지사항 목록을 불러오는 데 실패했습니다.", + "notice.error.saveFailed": "저장에 실패했습니다.", + "notice.error.deleteFailed": "삭제에 실패했습니다.", + "notice.filter.statusPlaceholder": "상태 필터", + "notice.filter.all": "전체", + "notice.filter.active": "활성", + "notice.filter.inactive": "비활성", + "notice.filter.searchPlaceholder": "제목 또는 내용으로 검색...", + "notice.list.total": "총", + "notice.list.countSuffix": "건", + "notice.list.refreshLabel": "새로고침", + "notice.list.empty": "공지사항이 없습니다.", + "notice.button.create": "등록", + "notice.button.editLabel": "수정", + "notice.button.deleteLabel": "삭제", + "notice.column.title": "제목", + "notice.column.status": "상태", + "notice.column.priority": "우선순위", + "notice.column.author": "작성자", + "notice.column.createdAt": "작성일", + "notice.column.actions": "관리", + "notice.card.author": "작성자", + "notice.card.createdAt": "작성일", + "notice.status.active": "활성", + "notice.status.inactive": "비활성", + "notice.priority.high": "높음", + "notice.priority.medium": "보통", + "notice.priority.low": "낮음", + "notice.form.titleCreate": "공지사항 등록", + "notice.form.titleEdit": "공지사항 수정", + "notice.form.descriptionCreate": "새로운 공지사항을 등록합니다.", + "notice.form.descriptionEdit": "공지사항 내용을 수정합니다.", + "notice.form.titleLabel": "제목", + "notice.form.titlePlaceholder": "공지사항 제목을 입력하세요", + "notice.form.contentLabel": "내용", + "notice.form.contentPlaceholder": "공지사항 내용을 입력하세요", + "notice.form.priorityLabel": "우선순위", + "notice.form.priorityPlaceholder": "우선순위 선택", + "notice.form.activeLabel": "활성화 (체크 시 공지사항이 사용자에게 표시됩니다)", + "notice.form.cancel": "취소", + "notice.form.save": "저장", + "notice.form.saving": "저장 중...", + "notice.validate.titleRequired": "제목을 입력해주세요.", + "notice.validate.contentRequired": "내용을 입력해주세요.", + "notice.delete.title": "공지사항 삭제", + "notice.delete.description": "아래 공지사항을 삭제하시겠습니까?", + "notice.delete.descriptionIrreversible": "이 작업은 되돌릴 수 없습니다.", + "notice.delete.cancel": "취소", + "notice.delete.confirm": "삭제", + "notice.delete.deleting": "삭제 중...", +}; + +function getPriorityLabel(priority: number): { langKey: string; variant: "default" | "secondary" | "destructive" | "outline" } { + if (priority >= 3) return { langKey: "notice.priority.high", variant: "destructive" }; + if (priority === 2) return { langKey: "notice.priority.medium", variant: "default" }; + return { langKey: "notice.priority.low", variant: "secondary" }; } function formatDate(dateStr: string): string { @@ -57,6 +174,8 @@ const EMPTY_FORM: CreateSystemNoticePayload = { }; export default function SystemNoticesPage() { + const { t } = usePageMultiLang({ keys: LANG_KEYS, defaults: DEFAULT_TEXTS, menuCode: "admin.systemNotices" }); + const [notices, setNotices] = useState([]); const [filteredNotices, setFilteredNotices] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -80,7 +199,7 @@ export default function SystemNoticesPage() { if (result.success && result.data) { setNotices(result.data); } else { - setErrorMsg(result.message || "공지사항 목록을 불러오는 데 실패했습니다."); + setErrorMsg(result.message || t("notice.error.loadFailed")); } setIsLoading(false); }, []); @@ -128,11 +247,11 @@ export default function SystemNoticesPage() { const handleSave = async () => { if (!formData.title.trim()) { - alert("제목을 입력해주세요."); + alert(t("notice.validate.titleRequired")); return; } if (!formData.content.trim()) { - alert("내용을 입력해주세요."); + alert(t("notice.validate.contentRequired")); return; } @@ -149,7 +268,7 @@ export default function SystemNoticesPage() { setIsFormOpen(false); await loadNotices(); } else { - alert(result.message || "저장에 실패했습니다."); + alert(result.message || t("notice.error.saveFailed")); } setIsSaving(false); }; @@ -162,7 +281,7 @@ export default function SystemNoticesPage() { setDeleteTarget(null); await loadNotices(); } else { - alert(result.message || "삭제에 실패했습니다."); + alert(result.message || t("notice.error.deleteFailed")); } setIsDeleting(false); }; @@ -170,33 +289,33 @@ export default function SystemNoticesPage() { const columns: RDVColumn[] = [ { key: "title", - label: "제목", + label: t("notice.column.title"), render: (_val, notice) => ( {notice.title} ), }, { key: "is_active", - label: "상태", + label: t("notice.column.status"), width: "100px", render: (_val, notice) => ( - {notice.is_active ? "활성" : "비활성"} + {notice.is_active ? t("notice.status.active") : t("notice.status.inactive")} ), }, { key: "priority", - label: "우선순위", + label: t("notice.column.priority"), width: "100px", render: (_val, notice) => { const p = getPriorityLabel(notice.priority); - return {p.label}; + return {t(p.langKey)}; }, }, { key: "created_by", - label: "작성자", + label: t("notice.column.author"), width: "120px", hideOnMobile: true, render: (_val, notice) => ( @@ -205,7 +324,7 @@ export default function SystemNoticesPage() { }, { key: "created_at", - label: "작성일", + label: t("notice.column.createdAt"), width: "120px", hideOnMobile: true, render: (_val, notice) => ( @@ -216,11 +335,11 @@ export default function SystemNoticesPage() { const cardFields: RDVCardField[] = [ { - label: "작성자", + label: t("notice.card.author"), render: (notice) => notice.created_by || "-", }, { - label: "작성일", + label: t("notice.card.createdAt"), render: (notice) => formatDate(notice.created_at), }, ]; @@ -230,9 +349,9 @@ export default function SystemNoticesPage() {

{/* 페이지 헤더 */}
-

시스템 공지사항

+

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

- 시스템 사용자에게 전달할 공지사항을 관리합니다. + {t("notice.page.description")}

@@ -240,11 +359,11 @@ export default function SystemNoticesPage() { {errorMsg && (
-

오류가 발생했습니다

+

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

@@ -259,12 +378,12 @@ export default function SystemNoticesPage() {
@@ -272,7 +391,7 @@ export default function SystemNoticesPage() {
setSearchText(e.target.value)} className="h-10 pl-10 text-sm" @@ -282,20 +401,20 @@ export default function SystemNoticesPage() {
- 총 {filteredNotices.length} 건 + {t("notice.list.total")} {filteredNotices.length} {t("notice.list.countSuffix")}
@@ -305,7 +424,7 @@ export default function SystemNoticesPage() { columns={columns} keyExtractor={(n) => String(n.id)} isLoading={isLoading} - emptyMessage="공지사항이 없습니다." + emptyMessage={t("notice.list.empty")} skeletonCount={5} cardTitle={(n) => n.title} cardHeaderRight={(n) => ( @@ -315,7 +434,7 @@ export default function SystemNoticesPage() { size="icon" className="h-8 w-8" onClick={() => handleOpenEdit(n)} - aria-label="수정" + aria-label={t("notice.button.editLabel")} > @@ -324,7 +443,7 @@ export default function SystemNoticesPage() { size="icon" className="h-8 w-8 text-destructive hover:text-destructive" onClick={() => setDeleteTarget(n)} - aria-label="삭제" + aria-label={t("notice.button.deleteLabel")} > @@ -335,14 +454,14 @@ export default function SystemNoticesPage() { return ( - {n.is_active ? "활성" : "비활성"} + {n.is_active ? t("notice.status.active") : t("notice.status.inactive")} - {p.label} + {t(p.langKey)} ); }} cardFields={cardFields} - actionsLabel="관리" + actionsLabel={t("notice.column.actions")} actionsWidth="120px" renderActions={(notice) => ( <> @@ -351,7 +470,7 @@ export default function SystemNoticesPage() { size="icon" className="h-8 w-8" onClick={() => handleOpenEdit(notice)} - aria-label="수정" + aria-label={t("notice.button.editLabel")} > @@ -360,7 +479,7 @@ export default function SystemNoticesPage() { size="icon" className="h-8 w-8 text-destructive hover:text-destructive" onClick={() => setDeleteTarget(notice)} - aria-label="삭제" + aria-label={t("notice.button.deleteLabel")} > @@ -374,43 +493,43 @@ export default function SystemNoticesPage() { - {editTarget ? "공지사항 수정" : "공지사항 등록"} + {editTarget ? t("notice.form.titleEdit") : t("notice.form.titleCreate")} - {editTarget ? "공지사항 내용을 수정합니다." : "새로운 공지사항을 등록합니다."} + {editTarget ? t("notice.form.descriptionEdit") : t("notice.form.descriptionCreate")}
setFormData((prev) => ({ ...prev, title: e.target.value }))} - placeholder="공지사항 제목을 입력하세요" + placeholder={t("notice.form.titlePlaceholder")} className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" />
항목{t("audit.changes.field")} - 변경 전 + {t("audit.changes.before")} - 변경 후 + {t("audit.changes.after")}