메뉴관리, 다국어관리, 토큰문제 해결

This commit is contained in:
kjs 2025-08-21 14:47:07 +09:00
parent 71d34ffd88
commit 86017c257d
14 changed files with 881 additions and 227 deletions

View File

@ -11,6 +11,7 @@ import { errorHandler } from "./middleware/errorHandler";
// 라우터 임포트
import authRoutes from "./routes/authRoutes";
import adminRoutes from "./routes/adminRoutes";
import multilangRoutes from "./routes/multilangRoutes";
// import userRoutes from './routes/userRoutes';
// import menuRoutes from './routes/menuRoutes';
@ -59,6 +60,7 @@ app.get("/health", (req, res) => {
// API 라우터
app.use("/api/auth", authRoutes);
app.use("/api/admin", adminRoutes);
app.use("/api/multilang", multilangRoutes);
// app.use('/api/users', userRoutes);
// app.use('/api/menus', menuRoutes);

View File

@ -242,3 +242,485 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
});
}
};
/**
* GET /api/admin/user-locale
* API
*/
export const getUserLocale = async (
req: AuthenticatedRequest,
res: Response
) => {
try {
logger.info("사용자 로케일 조회 요청", {
query: req.query,
user: req.user,
});
// 임시 더미 데이터 반환 (실제로는 데이터베이스에서 조회)
const userLocale = "ko"; // 기본값
const response = {
success: true,
data: userLocale,
message: "사용자 로케일 조회 성공",
};
logger.info("사용자 로케일 조회 성공", {
userLocale,
});
res.status(200).json(response);
} catch (error) {
logger.error("사용자 로케일 조회 실패", { error });
res.status(500).json({
success: false,
message: "사용자 로케일 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
};
/**
* GET /api/admin/companies
* API
*/
export const getCompanyList = async (
req: AuthenticatedRequest,
res: Response
) => {
try {
logger.info("회사 목록 조회 요청", {
query: req.query,
user: req.user,
});
// 임시 더미 데이터 반환 (실제로는 데이터베이스에서 조회)
const dummyCompanies = [
{
company_code: "ILSHIN",
company_name: "일신제강",
},
{
company_code: "HUTECH",
company_name: "후테크",
},
{
company_code: "DAIN",
company_name: "다인",
},
];
const response = {
success: true,
data: dummyCompanies,
message: "회사 목록 조회 성공",
};
logger.info("회사 목록 조회 성공", {
totalCount: dummyCompanies.length,
});
res.status(200).json(response);
} catch (error) {
logger.error("회사 목록 조회 실패", { error });
res.status(500).json({
success: false,
message: "회사 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
};
/**
* ( )
*/
export async function getLanguageList(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
logger.info("다국어 언어 목록 조회 요청");
// 더미 데이터 반환
const languages = [
{
langCode: "KR",
langName: "한국어",
langNative: "한국어",
isActive: "Y",
},
{
langCode: "EN",
langName: "English",
langNative: "English",
isActive: "Y",
},
{
langCode: "JP",
langName: "日本語",
langNative: "日本語",
isActive: "Y",
},
{ langCode: "CN", langName: "中文", langNative: "中文", isActive: "Y" },
];
const response: ApiResponse<any[]> = {
success: true,
message: "언어 목록 조회 성공",
data: languages,
};
res.status(200).json(response);
} catch (error) {
logger.error("언어 목록 조회 실패:", error);
res.status(500).json({
success: false,
message: "언어 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* ( )
*/
export async function getLangKeyList(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
logger.info("다국어 키 목록 조회 요청");
// 더미 데이터 반환
const langKeys = [
{
keyId: 1,
companyCode: "ILSHIN",
menuName: "사용자 관리",
langKey: "user.management.title",
description: "사용자 관리 페이지 제목",
isActive: "Y",
},
{
keyId: 2,
companyCode: "ILSHIN",
menuName: "메뉴 관리",
langKey: "menu.management.title",
description: "메뉴 관리 페이지 제목",
isActive: "Y",
},
{
keyId: 3,
companyCode: "HUTECH",
menuName: "대시보드",
langKey: "dashboard.title",
description: "대시보드 페이지 제목",
isActive: "Y",
},
];
const response: ApiResponse<any[]> = {
success: true,
message: "다국어 키 목록 조회 성공",
data: langKeys,
};
res.status(200).json(response);
} catch (error) {
logger.error("다국어 키 목록 조회 실패:", error);
res.status(500).json({
success: false,
message: "다국어 키 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* ( )
*/
export async function getLangTextList(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { keyId } = req.params;
logger.info(`다국어 텍스트 목록 조회 요청: keyId = ${keyId}`);
// 더미 데이터 반환
const langTexts = [
{
textId: 1,
keyId: parseInt(keyId),
langCode: "KR",
langText: "사용자 관리",
isActive: "Y",
},
{
textId: 2,
keyId: parseInt(keyId),
langCode: "EN",
langText: "User Management",
isActive: "Y",
},
{
textId: 3,
keyId: parseInt(keyId),
langCode: "JP",
langText: "ユーザー管理",
isActive: "Y",
},
];
const response: ApiResponse<any[]> = {
success: true,
message: "다국어 텍스트 목록 조회 성공",
data: langTexts,
};
res.status(200).json(response);
} catch (error) {
logger.error("다국어 텍스트 목록 조회 실패:", error);
res.status(500).json({
success: false,
message: "다국어 텍스트 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* ( )
*/
export async function saveLangTexts(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { keyId } = req.params;
const textData = req.body;
logger.info(`다국어 텍스트 저장 요청: keyId = ${keyId}`, { textData });
// 더미 응답
const response: ApiResponse<any> = {
success: true,
message: "다국어 텍스트 저장 성공",
data: { savedCount: textData.length },
};
res.status(200).json(response);
} catch (error) {
logger.error("다국어 텍스트 저장 실패:", error);
res.status(500).json({
success: false,
message: "다국어 텍스트 저장 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* ( )
*/
export async function saveLangKey(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const keyData = req.body;
logger.info("다국어 키 저장 요청", { keyData });
// 더미 응답
const response: ApiResponse<any> = {
success: true,
message: "다국어 키 저장 성공",
data: { keyId: Math.floor(Math.random() * 1000) + 1 },
};
res.status(200).json(response);
} catch (error) {
logger.error("다국어 키 저장 실패:", error);
res.status(500).json({
success: false,
message: "다국어 키 저장 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* ( )
*/
export async function updateLangKey(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { keyId } = req.params;
const keyData = req.body;
logger.info(`다국어 키 수정 요청: keyId = ${keyId}`, { keyData });
// 더미 응답
const response: ApiResponse<any> = {
success: true,
message: "다국어 키 수정 성공",
data: { keyId: parseInt(keyId) },
};
res.status(200).json(response);
} catch (error) {
logger.error("다국어 키 수정 실패:", error);
res.status(500).json({
success: false,
message: "다국어 키 수정 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* ( )
*/
export async function deleteLangKey(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { keyId } = req.params;
logger.info(`다국어 키 삭제 요청: keyId = ${keyId}`);
// 더미 응답
const response: ApiResponse<any> = {
success: true,
message: "다국어 키 삭제 성공",
data: { deletedKeyId: parseInt(keyId) },
};
res.status(200).json(response);
} catch (error) {
logger.error("다국어 키 삭제 실패:", error);
res.status(500).json({
success: false,
message: "다국어 키 삭제 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* ( )
*/
export async function toggleLangKeyStatus(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { keyId } = req.params;
logger.info(`다국어 키 상태 토글 요청: keyId = ${keyId}`);
// 더미 응답
const response: ApiResponse<any> = {
success: true,
message: "다국어 키 상태 토글 성공",
data: "활성화",
};
res.status(200).json(response);
} catch (error) {
logger.error("다국어 키 상태 토글 실패:", error);
res.status(500).json({
success: false,
message: "다국어 키 상태 토글 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* ( )
*/
export async function saveLanguage(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const langData = req.body;
logger.info("언어 저장 요청", { langData });
// 더미 응답
const response: ApiResponse<any> = {
success: true,
message: "언어 저장 성공",
data: { langCode: langData.langCode },
};
res.status(200).json(response);
} catch (error) {
logger.error("언어 저장 실패:", error);
res.status(500).json({
success: false,
message: "언어 저장 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* ( )
*/
export async function updateLanguage(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { langCode } = req.params;
const langData = req.body;
logger.info(`언어 수정 요청: langCode = ${langCode}`, { langData });
// 더미 응답
const response: ApiResponse<any> = {
success: true,
message: "언어 수정 성공",
data: { langCode },
};
res.status(200).json(response);
} catch (error) {
logger.error("언어 수정 실패:", error);
res.status(500).json({
success: false,
message: "언어 수정 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* ( )
*/
export async function toggleLanguageStatus(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { langCode } = req.params;
logger.info(`언어 상태 토글 요청: langCode = ${langCode}`);
// 더미 응답
const response: ApiResponse<any> = {
success: true,
message: "언어 상태 토글 성공",
data: "활성화",
};
res.status(200).json(response);
} catch (error) {
logger.error("언어 상태 토글 실패:", error);
res.status(500).json({
success: false,
message: "언어 상태 토글 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}

View File

@ -0,0 +1,44 @@
import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { logger } from "../utils/logger";
/**
* GET /api/multilang/user-text/:companyCode/:menuCode/:langKey
* API
*/
export const getUserText = async (req: AuthenticatedRequest, res: Response) => {
try {
const { companyCode, menuCode, langKey } = req.params;
const { userLang } = req.query;
logger.info("다국어 텍스트 조회 요청", {
companyCode,
menuCode,
langKey,
userLang,
user: req.user,
});
// 임시 더미 데이터 반환 (실제로는 데이터베이스에서 조회)
const dummyText = `${menuCode}_${langKey}_${userLang}`;
const response = {
success: true,
data: dummyText,
message: "다국어 텍스트 조회 성공",
};
logger.info("다국어 텍스트 조회 성공", {
text: dummyText,
});
res.status(200).json(response);
} catch (error) {
logger.error("다국어 텍스트 조회 실패", { error });
res.status(500).json({
success: false,
message: "다국어 텍스트 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
};

View File

@ -4,6 +4,19 @@ import {
getUserMenus,
getMenuInfo,
getUserList,
getCompanyList,
getUserLocale,
getLanguageList,
getLangKeyList,
getLangTextList,
saveLangTexts,
saveLangKey,
updateLangKey,
deleteLangKey,
toggleLangKeyStatus,
saveLanguage,
updateLanguage,
toggleLanguageStatus,
} from "../controllers/adminController";
import { authenticateToken } from "../middleware/authMiddleware";
@ -20,4 +33,23 @@ router.get("/menus/:menuId", getMenuInfo);
// 사용자 관리 API
router.get("/users", getUserList);
// 회사 관리 API
router.get("/companies", getCompanyList);
// 사용자 로케일 API
router.get("/user-locale", getUserLocale);
// 다국어 관리 API
router.get("/multilang/languages", getLanguageList);
router.get("/multilang/keys", getLangKeyList);
router.get("/multilang/keys/:keyId/texts", getLangTextList);
router.post("/multilang/keys/:keyId/texts", saveLangTexts);
router.post("/multilang/keys", saveLangKey);
router.put("/multilang/keys/:keyId", updateLangKey);
router.delete("/multilang/keys/:keyId", deleteLangKey);
router.put("/multilang/keys/:keyId/toggle", toggleLangKeyStatus);
router.post("/multilang/languages", saveLanguage);
router.put("/multilang/languages/:langCode", updateLanguage);
router.put("/multilang/languages/:langCode/toggle", toggleLanguageStatus);
export default router;

View File

@ -0,0 +1,13 @@
import { Router } from "express";
import { getUserText } from "../controllers/multilangController";
import { authenticateToken } from "../middleware/authMiddleware";
const router = Router();
// 모든 multilang 라우트에 인증 미들웨어 적용
router.use(authenticateToken);
// 다국어 텍스트 API
router.get("/user-text/:companyCode/:menuCode/:langKey", getUserText);
export default router;

View File

@ -186,7 +186,6 @@ export class AdminService {
FROM v_menu A
LEFT JOIN COMPANY_MNG CM ON A.COMPANY_CODE = CM.COMPANY_CODE
WHERE 1 = 1
AND (A.SEQ > 1 OR (A.SEQ = 0 AND LEVEL = 1))
ORDER BY PATH, SEQ
`;
@ -312,7 +311,6 @@ export class AdminService {
LEFT JOIN MULTI_LANG_KEY_MASTER MLKM_DESC ON A.LANG_KEY_DESC = MLKM_DESC.lang_key
LEFT JOIN MULTI_LANG_TEXT MLT_DESC ON MLKM_DESC.key_id = MLT_DESC.key_id AND MLT_DESC.lang_code = ${userLang}
WHERE 1 = 1
AND (A.SEQ > 1 OR (A.SEQ = 0 AND LEVEL = 1))
ORDER BY PATH, SEQ
`;

View File

@ -837,6 +837,9 @@ export const logger = winston.createLogger({
12. **JWT 토큰 관리**: 프론트엔드 API 클라이언트에서 JWT 토큰을 자동으로 포함하여 인증 문제 해결
13. **환경변수 관리**: Prisma 스키마에서 직접 데이터베이스 URL 설정으로 환경변수 로딩 문제 해결
14. **어드민 메뉴 인증**: 새 탭에서 열리는 어드민 페이지의 토큰 인증 문제 해결 - localStorage 공유 활용
15. **관리자 메뉴 내 페이지 이동 토큰 문제**: 레이아웃 레벨 토큰 확인 및 동기화 구현
16. **API 클라이언트 통일**: 모든 API에서 apiClient 사용으로 토큰 자동 전달 보장
17. **토큰 동기화 유틸리티**: localStorage와 sessionStorage 간 토큰 동기화 및 복원 기능
## 🔐 인증 및 보안 가이드
@ -972,6 +975,175 @@ const TokenManager = {
};
```
#### 토큰 동기화 유틸리티
```typescript
// lib/sessionManager.ts
export const tokenSync = {
// 토큰 상태 확인
checkToken: () => {
const token = localStorage.getItem("authToken");
console.log("🔍 토큰 상태 확인:", token ? "존재" : "없음");
return !!token;
},
// 토큰 강제 동기화 (다른 탭에서 설정된 토큰을 현재 탭에 복사)
forceSync: () => {
const token = localStorage.getItem("authToken");
if (token) {
// sessionStorage에도 복사
sessionStorage.setItem("authToken", token);
console.log("🔄 토큰 강제 동기화 완료");
return true;
}
return false;
},
// 토큰 복원 시도 (sessionStorage에서 복원)
restoreFromSession: () => {
const sessionToken = sessionStorage.getItem("authToken");
if (sessionToken) {
localStorage.setItem("authToken", sessionToken);
console.log("🔄 sessionStorage에서 토큰 복원 완료");
return true;
}
return false;
},
// 토큰 유효성 검증
validateToken: (token: string) => {
if (!token) return false;
try {
// JWT 토큰 구조 확인 (header.payload.signature)
const parts = token.split(".");
if (parts.length !== 3) return false;
// payload 디코딩 시도
const payload = JSON.parse(atob(parts[1]));
const now = Math.floor(Date.now() / 1000);
// 만료 시간 확인
if (payload.exp && payload.exp < now) {
console.log("❌ 토큰 만료됨");
return false;
}
console.log("✅ 토큰 유효성 검증 통과");
return true;
} catch (error) {
console.log("❌ 토큰 유효성 검증 실패:", error);
return false;
}
},
};
```
#### 관리자 레이아웃 토큰 확인
```typescript
// app/(main)/admin/layout.tsx
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const [isAuthorized, setIsAuthorized] = useState<boolean | null>(null);
// 토큰 확인 및 인증 상태 체크
useEffect(() => {
const checkToken = () => {
const token = localStorage.getItem("authToken");
// 토큰이 없으면 sessionStorage에서 복원 시도
if (!token && sessionToken) {
const restored = tokenSync.restoreFromSession();
if (restored) {
setIsAuthorized(true);
return;
}
}
// 토큰 유효성 검증
if (token && !tokenSync.validateToken(token)) {
localStorage.removeItem("authToken");
sessionStorage.removeItem("authToken");
setIsAuthorized(false);
return;
}
if (!token) {
setIsAuthorized(false);
return;
}
// 토큰이 있으면 인증된 것으로 간주
setIsAuthorized(true);
// 토큰 강제 동기화 (다른 탭과 동기화)
tokenSync.forceSync();
};
// 초기 토큰 확인
checkToken();
// localStorage 변경 이벤트 리스너 추가
const handleStorageChange = (e: StorageEvent) => {
if (e.key === "authToken") {
checkToken();
}
};
// 페이지 포커스 시 토큰 재확인
const handleFocus = () => {
checkToken();
};
window.addEventListener("storage", handleStorageChange);
window.addEventListener("focus", handleFocus);
return () => {
window.removeEventListener("storage", handleStorageChange);
window.removeEventListener("focus", handleFocus);
};
}, [pathname]);
}
```
#### API 클라이언트 통일
```typescript
// lib/api/user.ts - 수정 전 (fetch 사용)
async function apiCall<T = any>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
headers: {
"Content-Type": "application/json",
...options.headers,
},
credentials: "include",
...options,
});
// 토큰 수동 추가 필요
}
// lib/api/user.ts - 수정 후 (apiClient 사용)
export async function getUserList(params?: Record<string, any>) {
try {
const response = await apiClient.get("/admin/users", {
params: params,
});
// 토큰 자동 추가됨
return response.data;
} catch (error) {
console.error("❌ 사용자 목록 API 오류:", error);
throw error;
}
}
```
#### 백엔드 토큰 검증
```typescript
@ -1011,6 +1183,38 @@ export const authenticateToken = (
};
```
### 토큰 인증 문제 해결 완료 사항
#### ✅ 해결된 문제들
1. **어드민 메뉴 토큰 인증 문제**
- 새 탭에서 열리는 어드민 페이지의 토큰 공유 ✅
- localStorage 기반 토큰 동기화 ✅
2. **관리자 메뉴 내 페이지 이동 시 토큰 문제**
- 레이아웃 레벨에서 토큰 확인 로직 추가 ✅
- 실시간 토큰 동기화 및 검증 ✅
3. **사용자 관리 메뉴 특정 인증 문제**
- API 클라이언트 통일 (fetch → apiClient) ✅
- 토큰 자동 전달 활성화 ✅
#### 🔧 구현된 기능들
- **토큰 동기화 유틸리티**: `tokenSync` 모듈
- **강화된 인증 체크**: 레이아웃 레벨 토큰 검증
- **API 클라이언트 통일**: 모든 API에서 토큰 자동 전달
- **디버깅 도구**: 상세한 토큰 상태 확인 및 API 테스트
#### 📝 테스트 방법
1. **Admin 버튼 클릭** → 어드민 페이지 열기
2. **사이드바 메뉴 클릭** → 다른 관리자 페이지로 이동
3. **디버깅 페이지 확인**`/admin/debug-layout`에서 토큰 상태 확인
4. **API 테스트** → 각 메뉴에서 API 호출 정상 작동 확인
## 🎯 성공 지표
1. **성능 개선**: API 응답 시간 30% 단축
@ -1021,6 +1225,6 @@ export const authenticateToken = (
---
**마지막 업데이트**: 2024년 12월 20일
**버전**: 1.8.0
**버전**: 1.9.0
**작성자**: AI Assistant
**현재 상태**: Phase 1 완료, Phase 2-1A 완료, Phase 2-1B 완료, Phase 2-2A 완료 ✅ (메뉴 API 구현 완료, 어드민 메뉴 인증 문제 해결)
**현재 상태**: Phase 1 완료, Phase 2-1A 완료, Phase 2-1B 완료, Phase 2-2A 완료 ✅ (메뉴 API 구현 완료, 어드민 메뉴 인증 문제 해결, 토큰 인증 문제 완전 해결)

View File

@ -6,6 +6,7 @@ import { Settings, Menu, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { menuApi, MenuItem } from "@/lib/api/menu";
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
import { apiClient } from "@/lib/api/client";
import { toast } from "sonner";
import { useRouter, usePathname } from "next/navigation";
import {
@ -367,27 +368,19 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
});
// API 직접 호출로 현재 언어 사용
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
const companyCode = "*";
const [titleResponse, descriptionResponse] = await Promise.all([
fetch(
`${API_BASE_URL}/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.TITLE}?userLang=${currentUserLang}`,
{
credentials: "include",
headers: { "Content-Type": "application/json" },
},
apiClient.get(
`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.TITLE}?userLang=${currentUserLang}`,
),
fetch(
`${API_BASE_URL}/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.DESCRIPTION}?userLang=${currentUserLang}`,
{
credentials: "include",
headers: { "Content-Type": "application/json" },
},
apiClient.get(
`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.DESCRIPTION}?userLang=${currentUserLang}`,
),
]);
const [titleData, descriptionData] = await Promise.all([titleResponse.json(), descriptionResponse.json()]);
const titleData = titleResponse.data;
const descriptionData = descriptionResponse.data;
const title = titleData.success ? titleData.data : "메뉴 관리";
const description = descriptionData.success ? descriptionData.data : "시스템의 메뉴 구조와 권한을 관리합니다.";

View File

@ -11,6 +11,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { DataTable } from "@/components/common/DataTable";
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
import { useAuth } from "@/hooks/useAuth";
import { apiClient } from "@/lib/api/client";
interface Language {
langCode: string;
@ -93,8 +94,8 @@ export default function MultiLangPage() {
// 언어 목록 조회
const fetchLanguages = async () => {
try {
const response = await fetch("/api/multilang/languages");
const data = await response.json();
const response = await apiClient.get("/api/admin/multilang/languages");
const data = response.data;
if (data.success) {
setLanguages(data.data);
}
@ -106,8 +107,8 @@ export default function MultiLangPage() {
// 다국어 키 목록 조회
const fetchLangKeys = async () => {
try {
const response = await fetch(`/api/multilang/keys`);
const data = await response.json();
const response = await apiClient.get("/api/admin/multilang/keys");
const data = response.data;
if (data.success) {
setLangKeys(data.data);
}

View File

@ -31,6 +31,7 @@ import {
setTranslationCache,
} from "@/lib/utils/multilang";
import { useMultiLang } from "@/hooks/useMultiLang";
import { apiClient } from "@/lib/api/client";
type MenuType = "admin" | "user";
@ -148,24 +149,16 @@ export const MenuManagement: React.FC = () => {
const loadCompanies = async () => {
console.log(`🏢 회사 목록 조회 시작`);
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080"}/admin/companies`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
const response = await apiClient.get("/admin/companies");
if (response.ok) {
const data = await response.json();
if (data.success) {
console.log("🏢 회사 목록 응답:", data);
const companyList = data.data.map((company: any) => ({
code: company.company_code || company.companyCode,
name: company.company_name || company.companyName,
}));
console.log("🏢 변환된 회사 목록:", companyList);
setCompanies(companyList);
}
if (response.data.success) {
console.log("🏢 회사 목록 응답:", response.data);
const companyList = response.data.data.map((company: any) => ({
code: company.company_code || company.companyCode,
name: company.company_name || company.companyName,
}));
console.log("🏢 변환된 회사 목록:", companyList);
setCompanies(companyList);
}
} catch (error) {
console.error("❌ 회사 목록 조회 실패:", error);

View File

@ -13,6 +13,7 @@ import { LoadingSpinner } from "@/components/common/LoadingSpinner";
import { useAuth } from "@/hooks/useAuth";
import LangKeyModal from "./LangKeyModal";
import LanguageModal from "./LanguageModal";
import { apiClient } from "@/lib/api/client";
interface Language {
langCode: string;
@ -60,32 +61,18 @@ export default function MultiLangPage() {
const [companies, setCompanies] = useState<Array<{ code: string; name: string }>>([]);
// API 기본 URL 설정
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
// 회사 목록 조회
const fetchCompanies = async () => {
try {
console.log("회사 목록 조회 시작:", `${API_BASE_URL}/multilang/companies`);
const response = await fetch(`${API_BASE_URL}/multilang/companies`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
console.log("회사 목록 응답 상태:", response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log("회사 목록 응답 데이터:", data);
console.log("회사 목록 조회 시작");
const response = await apiClient.get("/api/admin/companies");
console.log("회사 목록 응답 데이터:", response.data);
const data = response.data;
if (data.success) {
const companyList = data.data.map((company: any) => ({
code: company.companyCode,
name: company.companyName,
code: company.company_code,
name: company.company_name,
}));
console.log("변환된 회사 목록:", companyList);
setCompanies(companyList);
@ -100,13 +87,8 @@ export default function MultiLangPage() {
// 언어 목록 조회
const fetchLanguages = async () => {
try {
const response = await fetch(`${API_BASE_URL}/multilang/languages`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
const response = await apiClient.get("/api/admin/multilang/languages");
const data = response.data;
if (data.success) {
setLanguages(data.data);
}
@ -118,13 +100,8 @@ export default function MultiLangPage() {
// 다국어 키 목록 조회
const fetchLangKeys = async () => {
try {
const response = await fetch(`${API_BASE_URL}/multilang/keys`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
const response = await apiClient.get("/api/admin/multilang/keys");
const data = response.data;
if (data.success) {
console.log("✅ 전체 키 목록 로드:", data.data.length, "개");
setLangKeys(data.data);
@ -170,13 +147,8 @@ export default function MultiLangPage() {
const fetchLangTexts = async (keyId: number) => {
try {
console.log("다국어 텍스트 조회 시작: keyId =", keyId);
const response = await fetch(`${API_BASE_URL}/multilang/keys/${keyId}/texts`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
const response = await apiClient.get(`/api/admin/multilang/keys/${keyId}/texts`);
const data = response.data;
console.log("다국어 텍스트 조회 응답:", data);
if (data.success) {
setLangTexts(data.data);
@ -231,22 +203,12 @@ export default function MultiLangPage() {
if (!selectedKey) return;
try {
const response = await fetch(`${API_BASE_URL}/multilang/keys/${selectedKey.keyId}/texts`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(editingTexts),
});
if (response.ok) {
const data = await response.json();
if (data.success) {
alert("저장되었습니다.");
// 저장 후 다시 조회
fetchLangTexts(selectedKey.keyId);
}
const response = await apiClient.post(`/api/admin/multilang/keys/${selectedKey.keyId}/texts`, editingTexts);
const data = response.data;
if (data.success) {
alert("저장되었습니다.");
// 저장 후 다시 조회
fetchLangTexts(selectedKey.keyId);
}
} catch (error) {
console.error("텍스트 저장 실패:", error);
@ -275,30 +237,20 @@ export default function MultiLangPage() {
// 언어 저장 (추가/수정)
const handleSaveLanguage = async (languageData: any) => {
try {
const url = editingLanguage
? `${API_BASE_URL}/multilang/languages/${editingLanguage.langCode}`
: `${API_BASE_URL}/multilang/languages`;
const requestData = {
...languageData,
createdBy: user?.userId || "admin",
updatedBy: user?.userId || "admin",
};
const method = editingLanguage ? "PUT" : "POST";
const response = await fetch(url, {
method,
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...languageData,
createdBy: user?.userId || "admin",
updatedBy: user?.userId || "admin",
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
let response;
if (editingLanguage) {
response = await apiClient.put(`/api/admin/multilang/languages/${editingLanguage.langCode}`, requestData);
} else {
response = await apiClient.post("/api/admin/multilang/languages", requestData);
}
const result = await response.json();
const result = response.data;
if (result.success) {
alert(editingLanguage ? "언어가 수정되었습니다." : "언어가 추가되었습니다.");
@ -384,28 +336,22 @@ export default function MultiLangPage() {
// 언어 키 저장 (추가/수정)
const handleSaveKey = async (keyData: any) => {
try {
const url = editingKey ? `${API_BASE_URL}/multilang/keys/${editingKey.keyId}` : `${API_BASE_URL}/multilang/keys`;
const method = editingKey ? "PUT" : "POST";
const requestData = {
...keyData,
createdBy: user?.userId || "admin",
updatedBy: user?.userId || "admin",
};
const response = await fetch(url, {
method,
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestData),
});
let response;
if (editingKey) {
response = await apiClient.put(`/api/admin/multilang/keys/${editingKey.keyId}`, requestData);
} else {
response = await apiClient.post("/api/admin/multilang/keys", requestData);
}
const data = await response.json();
const data = response.data;
if (response.ok && data.success) {
if (data.success) {
alert(editingKey ? "언어 키가 수정되었습니다." : "언어 키가 추가되었습니다.");
fetchLangKeys(); // 목록 새로고침
setIsModalOpen(false);
@ -437,15 +383,8 @@ export default function MultiLangPage() {
// 키 상태 토글
const handleToggleStatus = async (keyId: number) => {
try {
const response = await fetch(`${API_BASE_URL}/multilang/keys/${keyId}/toggle`, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
const response = await apiClient.put(`/api/admin/multilang/keys/${keyId}/toggle`);
const data = response.data;
if (data.success) {
alert(`키가 ${data.data}되었습니다.`);
fetchLangKeys();
@ -461,15 +400,8 @@ export default function MultiLangPage() {
// 언어 상태 토글
const handleToggleLanguageStatus = async (langCode: string) => {
try {
const response = await fetch(`${API_BASE_URL}/multilang/languages/${langCode}/toggle`, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
const response = await apiClient.put(`/api/admin/multilang/languages/${langCode}/toggle`);
const data = response.data;
if (data.success) {
alert(`언어가 ${data.data}되었습니다.`);
fetchLanguages();
@ -509,17 +441,11 @@ export default function MultiLangPage() {
try {
const deletePromises = Array.from(selectedKeys).map((keyId) =>
fetch(`${API_BASE_URL}/multilang/keys/${keyId}`, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
}),
apiClient.delete(`/api/admin/multilang/keys/${keyId}`),
);
const responses = await Promise.all(deletePromises);
const allSuccess = responses.every((response) => response.ok);
const allSuccess = responses.every((response) => response.data.success);
if (allSuccess) {
alert(`${selectedKeys.size}개의 언어 키가 영구적으로 삭제되었습니다.`);
@ -546,22 +472,13 @@ export default function MultiLangPage() {
}
try {
const response = await fetch(`${API_BASE_URL}/multilang/keys/${keyId}`, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
if (response.ok) {
const data = await response.json();
if (data.success) {
alert("언어 키가 영구적으로 삭제되었습니다.");
fetchLangKeys(); // 목록 새로고침
if (selectedKey && selectedKey.keyId === keyId) {
handleCancel(); // 선택된 키가 삭제된 경우 편집 영역 닫기
}
const response = await apiClient.delete(`/api/admin/multilang/keys/${keyId}`);
const data = response.data;
if (data.success) {
alert("언어 키가 영구적으로 삭제되었습니다.");
fetchLangKeys(); // 목록 새로고침
if (selectedKey && selectedKey.keyId === keyId) {
handleCancel(); // 선택된 키가 삭제된 경우 편집 영역 닫기
}
}
} catch (error) {

View File

@ -9,6 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { apiClient } from "@/lib/api/client";
interface Language {
langCode: string;
@ -94,8 +95,8 @@ export function LangKeyModal({
const fetchLangTexts = async (keyId: number) => {
try {
const response = await fetch(`/api/multilang/keys/${keyId}/texts`);
const data = await response.json();
const response = await apiClient.get(`/api/admin/multilang/keys/${keyId}/texts`);
const data = response.data;
if (data.success) {
const texts = data.data;
const allTexts = languages.map((lang) => {

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from "react";
import { setTranslationCache } from "@/lib/utils/multilang";
import { apiClient } from "@/lib/api/client";
interface UseMultiLangOptions {
companyCode?: string;
@ -35,32 +36,24 @@ export function useMultiLang(options: UseMultiLangOptions = {}) {
const fetchUserLocale = async () => {
try {
console.log("🔍 사용자 로케일 조회 시작");
const response = await fetch(`${API_BASE_URL}/admin/user-locale`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
const response = await apiClient.get("/admin/user-locale");
if (response.ok) {
const data = await response.json();
if (data.success && data.data) {
const userLocale = data.data;
console.log("✅ 사용자 로케일 조회 성공:", userLocale);
if (response.data.success && response.data.data) {
const userLocale = response.data.data;
console.log("✅ 사용자 로케일 조회 성공:", userLocale);
// 사용자 로케일을 데이터베이스 언어 코드로 매핑
const langMapping: Record<string, string> = {
ko: "KR",
en: "US",
ja: "JP",
zh: "CN",
};
// 사용자 로케일을 데이터베이스 언어 코드로 매핑
const langMapping: Record<string, string> = {
ko: "KR",
en: "US",
ja: "JP",
zh: "CN",
};
const mappedLang = langMapping[userLocale] || userLocale;
console.log("🔄 언어 매핑:", userLocale, "->", mappedLang);
setUserLang(mappedLang);
return;
}
const mappedLang = langMapping[userLocale] || userLocale;
console.log("🔄 언어 매핑:", userLocale, "->", mappedLang);
setUserLang(mappedLang);
return;
}
// API 호출 실패 시 브라우저 언어 사용
@ -102,35 +95,21 @@ export function useMultiLang(options: UseMultiLangOptions = {}) {
console.log(`🔍 다국어 텍스트 요청:`, { menuCode, langKey, userLang, companyCode });
try {
const url = `${API_BASE_URL}/multilang/user-text/${companyCode}/${menuCode}/${langKey}?userLang=${userLang}`;
const url = `/multilang/user-text/${companyCode}/${menuCode}/${langKey}?userLang=${userLang}`;
console.log(`📡 API 요청 URL:`, url);
const response = await fetch(url, {
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
const response = await apiClient.get(url);
console.log(`📡 API 응답 상태:`, response.status, response.statusText);
// HTTP 상태 코드 확인
if (!response.ok) {
console.warn(`❌ 다국어 API 오류: ${response.status} ${response.statusText}`);
console.log(`🔄 fallback 반환:`, fallback || langKey);
return fallback || langKey;
}
const data = await response.json();
if (data.success && data.data) {
if (response.data.success && response.data.data) {
// 개별 번역 텍스트를 캐시에 저장
const cacheKey = `${menuCode}.${langKey}`;
const currentCache = (window as any).__TRANSLATION_CACHE || {};
currentCache[cacheKey] = data.data;
currentCache[cacheKey] = response.data.data;
(window as any).__TRANSLATION_CACHE = currentCache;
return data.data;
return response.data.data;
}
// 실패 시 fallback 또는 키 반환

View File

@ -1,4 +1,5 @@
import { useMultiLang } from "@/hooks/useMultiLang";
import { apiClient } from "../api/client";
// 메뉴 관리 화면 다국어 키 상수
export const MENU_MANAGEMENT_KEYS = {
@ -194,18 +195,12 @@ export const getMenuTextSync = (key: string, params?: Record<string, any>): stri
// 비동기적으로 번역 로드 (백그라운드에서)
if (typeof window !== "undefined") {
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
const companyCode = "*";
fetch(`${API_BASE_URL}/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${key}?userLang=${userLang}`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((data) => {
if (data.success && data.data) {
apiClient
.get(`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${key}?userLang=${userLang}`)
.then((response) => {
if (response.data.success && response.data.data) {
// 개별 캐시에 저장
const currentCache = (window as any).__TRANSLATION_CACHE || {};
currentCache[cacheKey] = data.data;