메뉴관리, 다국어관리, 토큰문제 해결
This commit is contained in:
parent
71d34ffd88
commit
86017c257d
|
|
@ -11,6 +11,7 @@ import { errorHandler } from "./middleware/errorHandler";
|
||||||
// 라우터 임포트
|
// 라우터 임포트
|
||||||
import authRoutes from "./routes/authRoutes";
|
import authRoutes from "./routes/authRoutes";
|
||||||
import adminRoutes from "./routes/adminRoutes";
|
import adminRoutes from "./routes/adminRoutes";
|
||||||
|
import multilangRoutes from "./routes/multilangRoutes";
|
||||||
// import userRoutes from './routes/userRoutes';
|
// import userRoutes from './routes/userRoutes';
|
||||||
// import menuRoutes from './routes/menuRoutes';
|
// import menuRoutes from './routes/menuRoutes';
|
||||||
|
|
||||||
|
|
@ -59,6 +60,7 @@ app.get("/health", (req, res) => {
|
||||||
// API 라우터
|
// API 라우터
|
||||||
app.use("/api/auth", authRoutes);
|
app.use("/api/auth", authRoutes);
|
||||||
app.use("/api/admin", adminRoutes);
|
app.use("/api/admin", adminRoutes);
|
||||||
|
app.use("/api/multilang", multilangRoutes);
|
||||||
// app.use('/api/users', userRoutes);
|
// app.use('/api/users', userRoutes);
|
||||||
// app.use('/api/menus', menuRoutes);
|
// app.use('/api/menus', menuRoutes);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -4,6 +4,19 @@ import {
|
||||||
getUserMenus,
|
getUserMenus,
|
||||||
getMenuInfo,
|
getMenuInfo,
|
||||||
getUserList,
|
getUserList,
|
||||||
|
getCompanyList,
|
||||||
|
getUserLocale,
|
||||||
|
getLanguageList,
|
||||||
|
getLangKeyList,
|
||||||
|
getLangTextList,
|
||||||
|
saveLangTexts,
|
||||||
|
saveLangKey,
|
||||||
|
updateLangKey,
|
||||||
|
deleteLangKey,
|
||||||
|
toggleLangKeyStatus,
|
||||||
|
saveLanguage,
|
||||||
|
updateLanguage,
|
||||||
|
toggleLanguageStatus,
|
||||||
} from "../controllers/adminController";
|
} from "../controllers/adminController";
|
||||||
import { authenticateToken } from "../middleware/authMiddleware";
|
import { authenticateToken } from "../middleware/authMiddleware";
|
||||||
|
|
||||||
|
|
@ -20,4 +33,23 @@ router.get("/menus/:menuId", getMenuInfo);
|
||||||
// 사용자 관리 API
|
// 사용자 관리 API
|
||||||
router.get("/users", getUserList);
|
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;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -186,7 +186,6 @@ export class AdminService {
|
||||||
FROM v_menu A
|
FROM v_menu A
|
||||||
LEFT JOIN COMPANY_MNG CM ON A.COMPANY_CODE = CM.COMPANY_CODE
|
LEFT JOIN COMPANY_MNG CM ON A.COMPANY_CODE = CM.COMPANY_CODE
|
||||||
WHERE 1 = 1
|
WHERE 1 = 1
|
||||||
AND (A.SEQ > 1 OR (A.SEQ = 0 AND LEVEL = 1))
|
|
||||||
ORDER BY PATH, SEQ
|
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_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}
|
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
|
WHERE 1 = 1
|
||||||
AND (A.SEQ > 1 OR (A.SEQ = 0 AND LEVEL = 1))
|
|
||||||
ORDER BY PATH, SEQ
|
ORDER BY PATH, SEQ
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -837,6 +837,9 @@ export const logger = winston.createLogger({
|
||||||
12. **JWT 토큰 관리**: 프론트엔드 API 클라이언트에서 JWT 토큰을 자동으로 포함하여 인증 문제 해결
|
12. **JWT 토큰 관리**: 프론트엔드 API 클라이언트에서 JWT 토큰을 자동으로 포함하여 인증 문제 해결
|
||||||
13. **환경변수 관리**: Prisma 스키마에서 직접 데이터베이스 URL 설정으로 환경변수 로딩 문제 해결
|
13. **환경변수 관리**: Prisma 스키마에서 직접 데이터베이스 URL 설정으로 환경변수 로딩 문제 해결
|
||||||
14. **어드민 메뉴 인증**: 새 탭에서 열리는 어드민 페이지의 토큰 인증 문제 해결 - localStorage 공유 활용
|
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
|
```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% 단축
|
1. **성능 개선**: API 응답 시간 30% 단축
|
||||||
|
|
@ -1021,6 +1225,6 @@ export const authenticateToken = (
|
||||||
---
|
---
|
||||||
|
|
||||||
**마지막 업데이트**: 2024년 12월 20일
|
**마지막 업데이트**: 2024년 12월 20일
|
||||||
**버전**: 1.8.0
|
**버전**: 1.9.0
|
||||||
**작성자**: AI Assistant
|
**작성자**: 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 구현 완료, 어드민 메뉴 인증 문제 해결, 토큰 인증 문제 완전 해결)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { Settings, Menu, X } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { menuApi, MenuItem } from "@/lib/api/menu";
|
import { menuApi, MenuItem } from "@/lib/api/menu";
|
||||||
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useRouter, usePathname } from "next/navigation";
|
import { useRouter, usePathname } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
|
|
@ -367,27 +368,19 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
||||||
});
|
});
|
||||||
|
|
||||||
// API 직접 호출로 현재 언어 사용
|
// API 직접 호출로 현재 언어 사용
|
||||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
|
|
||||||
const companyCode = "*";
|
const companyCode = "*";
|
||||||
|
|
||||||
const [titleResponse, descriptionResponse] = await Promise.all([
|
const [titleResponse, descriptionResponse] = await Promise.all([
|
||||||
fetch(
|
apiClient.get(
|
||||||
`${API_BASE_URL}/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.TITLE}?userLang=${currentUserLang}`,
|
`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.TITLE}?userLang=${currentUserLang}`,
|
||||||
{
|
|
||||||
credentials: "include",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
fetch(
|
apiClient.get(
|
||||||
`${API_BASE_URL}/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.DESCRIPTION}?userLang=${currentUserLang}`,
|
`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.DESCRIPTION}?userLang=${currentUserLang}`,
|
||||||
{
|
|
||||||
credentials: "include",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
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 title = titleData.success ? titleData.data : "메뉴 관리";
|
||||||
const description = descriptionData.success ? descriptionData.data : "시스템의 메뉴 구조와 권한을 관리합니다.";
|
const description = descriptionData.success ? descriptionData.data : "시스템의 메뉴 구조와 권한을 관리합니다.";
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { DataTable } from "@/components/common/DataTable";
|
import { DataTable } from "@/components/common/DataTable";
|
||||||
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
interface Language {
|
interface Language {
|
||||||
langCode: string;
|
langCode: string;
|
||||||
|
|
@ -93,8 +94,8 @@ export default function MultiLangPage() {
|
||||||
// 언어 목록 조회
|
// 언어 목록 조회
|
||||||
const fetchLanguages = async () => {
|
const fetchLanguages = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/multilang/languages");
|
const response = await apiClient.get("/api/admin/multilang/languages");
|
||||||
const data = await response.json();
|
const data = response.data;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setLanguages(data.data);
|
setLanguages(data.data);
|
||||||
}
|
}
|
||||||
|
|
@ -106,8 +107,8 @@ export default function MultiLangPage() {
|
||||||
// 다국어 키 목록 조회
|
// 다국어 키 목록 조회
|
||||||
const fetchLangKeys = async () => {
|
const fetchLangKeys = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/multilang/keys`);
|
const response = await apiClient.get("/api/admin/multilang/keys");
|
||||||
const data = await response.json();
|
const data = response.data;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setLangKeys(data.data);
|
setLangKeys(data.data);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import {
|
||||||
setTranslationCache,
|
setTranslationCache,
|
||||||
} from "@/lib/utils/multilang";
|
} from "@/lib/utils/multilang";
|
||||||
import { useMultiLang } from "@/hooks/useMultiLang";
|
import { useMultiLang } from "@/hooks/useMultiLang";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
type MenuType = "admin" | "user";
|
type MenuType = "admin" | "user";
|
||||||
|
|
||||||
|
|
@ -148,24 +149,16 @@ export const MenuManagement: React.FC = () => {
|
||||||
const loadCompanies = async () => {
|
const loadCompanies = async () => {
|
||||||
console.log(`🏢 회사 목록 조회 시작`);
|
console.log(`🏢 회사 목록 조회 시작`);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080"}/admin/companies`, {
|
const response = await apiClient.get("/admin/companies");
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.data.success) {
|
||||||
const data = await response.json();
|
console.log("🏢 회사 목록 응답:", response.data);
|
||||||
if (data.success) {
|
const companyList = response.data.data.map((company: any) => ({
|
||||||
console.log("🏢 회사 목록 응답:", data);
|
code: company.company_code || company.companyCode,
|
||||||
const companyList = data.data.map((company: any) => ({
|
name: company.company_name || company.companyName,
|
||||||
code: company.company_code || company.companyCode,
|
}));
|
||||||
name: company.company_name || company.companyName,
|
console.log("🏢 변환된 회사 목록:", companyList);
|
||||||
}));
|
setCompanies(companyList);
|
||||||
console.log("🏢 변환된 회사 목록:", companyList);
|
|
||||||
setCompanies(companyList);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ 회사 목록 조회 실패:", error);
|
console.error("❌ 회사 목록 조회 실패:", error);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import LangKeyModal from "./LangKeyModal";
|
import LangKeyModal from "./LangKeyModal";
|
||||||
import LanguageModal from "./LanguageModal";
|
import LanguageModal from "./LanguageModal";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
interface Language {
|
interface Language {
|
||||||
langCode: string;
|
langCode: string;
|
||||||
|
|
@ -60,32 +61,18 @@ export default function MultiLangPage() {
|
||||||
|
|
||||||
const [companies, setCompanies] = useState<Array<{ code: string; name: string }>>([]);
|
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 () => {
|
const fetchCompanies = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("회사 목록 조회 시작:", `${API_BASE_URL}/multilang/companies`);
|
console.log("회사 목록 조회 시작");
|
||||||
const response = await fetch(`${API_BASE_URL}/multilang/companies`, {
|
const response = await apiClient.get("/api/admin/companies");
|
||||||
credentials: "include",
|
console.log("회사 목록 응답 데이터:", response.data);
|
||||||
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);
|
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const companyList = data.data.map((company: any) => ({
|
const companyList = data.data.map((company: any) => ({
|
||||||
code: company.companyCode,
|
code: company.company_code,
|
||||||
name: company.companyName,
|
name: company.company_name,
|
||||||
}));
|
}));
|
||||||
console.log("변환된 회사 목록:", companyList);
|
console.log("변환된 회사 목록:", companyList);
|
||||||
setCompanies(companyList);
|
setCompanies(companyList);
|
||||||
|
|
@ -100,13 +87,8 @@ export default function MultiLangPage() {
|
||||||
// 언어 목록 조회
|
// 언어 목록 조회
|
||||||
const fetchLanguages = async () => {
|
const fetchLanguages = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/multilang/languages`, {
|
const response = await apiClient.get("/api/admin/multilang/languages");
|
||||||
credentials: "include",
|
const data = response.data;
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setLanguages(data.data);
|
setLanguages(data.data);
|
||||||
}
|
}
|
||||||
|
|
@ -118,13 +100,8 @@ export default function MultiLangPage() {
|
||||||
// 다국어 키 목록 조회
|
// 다국어 키 목록 조회
|
||||||
const fetchLangKeys = async () => {
|
const fetchLangKeys = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/multilang/keys`, {
|
const response = await apiClient.get("/api/admin/multilang/keys");
|
||||||
credentials: "include",
|
const data = response.data;
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
console.log("✅ 전체 키 목록 로드:", data.data.length, "개");
|
console.log("✅ 전체 키 목록 로드:", data.data.length, "개");
|
||||||
setLangKeys(data.data);
|
setLangKeys(data.data);
|
||||||
|
|
@ -170,13 +147,8 @@ export default function MultiLangPage() {
|
||||||
const fetchLangTexts = async (keyId: number) => {
|
const fetchLangTexts = async (keyId: number) => {
|
||||||
try {
|
try {
|
||||||
console.log("다국어 텍스트 조회 시작: keyId =", keyId);
|
console.log("다국어 텍스트 조회 시작: keyId =", keyId);
|
||||||
const response = await fetch(`${API_BASE_URL}/multilang/keys/${keyId}/texts`, {
|
const response = await apiClient.get(`/api/admin/multilang/keys/${keyId}/texts`);
|
||||||
credentials: "include",
|
const data = response.data;
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
console.log("다국어 텍스트 조회 응답:", data);
|
console.log("다국어 텍스트 조회 응답:", data);
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setLangTexts(data.data);
|
setLangTexts(data.data);
|
||||||
|
|
@ -231,22 +203,12 @@ export default function MultiLangPage() {
|
||||||
if (!selectedKey) return;
|
if (!selectedKey) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/multilang/keys/${selectedKey.keyId}/texts`, {
|
const response = await apiClient.post(`/api/admin/multilang/keys/${selectedKey.keyId}/texts`, editingTexts);
|
||||||
method: "POST",
|
const data = response.data;
|
||||||
credentials: "include",
|
if (data.success) {
|
||||||
headers: {
|
alert("저장되었습니다.");
|
||||||
"Content-Type": "application/json",
|
// 저장 후 다시 조회
|
||||||
},
|
fetchLangTexts(selectedKey.keyId);
|
||||||
body: JSON.stringify(editingTexts),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
alert("저장되었습니다.");
|
|
||||||
// 저장 후 다시 조회
|
|
||||||
fetchLangTexts(selectedKey.keyId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("텍스트 저장 실패:", error);
|
console.error("텍스트 저장 실패:", error);
|
||||||
|
|
@ -275,30 +237,20 @@ export default function MultiLangPage() {
|
||||||
// 언어 저장 (추가/수정)
|
// 언어 저장 (추가/수정)
|
||||||
const handleSaveLanguage = async (languageData: any) => {
|
const handleSaveLanguage = async (languageData: any) => {
|
||||||
try {
|
try {
|
||||||
const url = editingLanguage
|
const requestData = {
|
||||||
? `${API_BASE_URL}/multilang/languages/${editingLanguage.langCode}`
|
...languageData,
|
||||||
: `${API_BASE_URL}/multilang/languages`;
|
createdBy: user?.userId || "admin",
|
||||||
|
updatedBy: user?.userId || "admin",
|
||||||
|
};
|
||||||
|
|
||||||
const method = editingLanguage ? "PUT" : "POST";
|
let response;
|
||||||
|
if (editingLanguage) {
|
||||||
const response = await fetch(url, {
|
response = await apiClient.put(`/api/admin/multilang/languages/${editingLanguage.langCode}`, requestData);
|
||||||
method,
|
} else {
|
||||||
credentials: "include",
|
response = await apiClient.post("/api/admin/multilang/languages", requestData);
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = response.data;
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert(editingLanguage ? "언어가 수정되었습니다." : "언어가 추가되었습니다.");
|
alert(editingLanguage ? "언어가 수정되었습니다." : "언어가 추가되었습니다.");
|
||||||
|
|
@ -384,28 +336,22 @@ export default function MultiLangPage() {
|
||||||
// 언어 키 저장 (추가/수정)
|
// 언어 키 저장 (추가/수정)
|
||||||
const handleSaveKey = async (keyData: any) => {
|
const handleSaveKey = async (keyData: any) => {
|
||||||
try {
|
try {
|
||||||
const url = editingKey ? `${API_BASE_URL}/multilang/keys/${editingKey.keyId}` : `${API_BASE_URL}/multilang/keys`;
|
|
||||||
|
|
||||||
const method = editingKey ? "PUT" : "POST";
|
|
||||||
|
|
||||||
const requestData = {
|
const requestData = {
|
||||||
...keyData,
|
...keyData,
|
||||||
createdBy: user?.userId || "admin",
|
createdBy: user?.userId || "admin",
|
||||||
updatedBy: user?.userId || "admin",
|
updatedBy: user?.userId || "admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(url, {
|
let response;
|
||||||
method,
|
if (editingKey) {
|
||||||
credentials: "include",
|
response = await apiClient.put(`/api/admin/multilang/keys/${editingKey.keyId}`, requestData);
|
||||||
headers: {
|
} else {
|
||||||
"Content-Type": "application/json",
|
response = await apiClient.post("/api/admin/multilang/keys", requestData);
|
||||||
},
|
}
|
||||||
body: JSON.stringify(requestData),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = response.data;
|
||||||
|
|
||||||
if (response.ok && data.success) {
|
if (data.success) {
|
||||||
alert(editingKey ? "언어 키가 수정되었습니다." : "언어 키가 추가되었습니다.");
|
alert(editingKey ? "언어 키가 수정되었습니다." : "언어 키가 추가되었습니다.");
|
||||||
fetchLangKeys(); // 목록 새로고침
|
fetchLangKeys(); // 목록 새로고침
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
|
|
@ -437,15 +383,8 @@ export default function MultiLangPage() {
|
||||||
// 키 상태 토글
|
// 키 상태 토글
|
||||||
const handleToggleStatus = async (keyId: number) => {
|
const handleToggleStatus = async (keyId: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/multilang/keys/${keyId}/toggle`, {
|
const response = await apiClient.put(`/api/admin/multilang/keys/${keyId}/toggle`);
|
||||||
method: "PUT",
|
const data = response.data;
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert(`키가 ${data.data}되었습니다.`);
|
alert(`키가 ${data.data}되었습니다.`);
|
||||||
fetchLangKeys();
|
fetchLangKeys();
|
||||||
|
|
@ -461,15 +400,8 @@ export default function MultiLangPage() {
|
||||||
// 언어 상태 토글
|
// 언어 상태 토글
|
||||||
const handleToggleLanguageStatus = async (langCode: string) => {
|
const handleToggleLanguageStatus = async (langCode: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/multilang/languages/${langCode}/toggle`, {
|
const response = await apiClient.put(`/api/admin/multilang/languages/${langCode}/toggle`);
|
||||||
method: "PUT",
|
const data = response.data;
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert(`언어가 ${data.data}되었습니다.`);
|
alert(`언어가 ${data.data}되었습니다.`);
|
||||||
fetchLanguages();
|
fetchLanguages();
|
||||||
|
|
@ -509,17 +441,11 @@ export default function MultiLangPage() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const deletePromises = Array.from(selectedKeys).map((keyId) =>
|
const deletePromises = Array.from(selectedKeys).map((keyId) =>
|
||||||
fetch(`${API_BASE_URL}/multilang/keys/${keyId}`, {
|
apiClient.delete(`/api/admin/multilang/keys/${keyId}`),
|
||||||
method: "DELETE",
|
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const responses = await Promise.all(deletePromises);
|
const responses = await Promise.all(deletePromises);
|
||||||
const allSuccess = responses.every((response) => response.ok);
|
const allSuccess = responses.every((response) => response.data.success);
|
||||||
|
|
||||||
if (allSuccess) {
|
if (allSuccess) {
|
||||||
alert(`${selectedKeys.size}개의 언어 키가 영구적으로 삭제되었습니다.`);
|
alert(`${selectedKeys.size}개의 언어 키가 영구적으로 삭제되었습니다.`);
|
||||||
|
|
@ -546,22 +472,13 @@ export default function MultiLangPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/multilang/keys/${keyId}`, {
|
const response = await apiClient.delete(`/api/admin/multilang/keys/${keyId}`);
|
||||||
method: "DELETE",
|
const data = response.data;
|
||||||
credentials: "include",
|
if (data.success) {
|
||||||
headers: {
|
alert("언어 키가 영구적으로 삭제되었습니다.");
|
||||||
"Content-Type": "application/json",
|
fetchLangKeys(); // 목록 새로고침
|
||||||
},
|
if (selectedKey && selectedKey.keyId === keyId) {
|
||||||
});
|
handleCancel(); // 선택된 키가 삭제된 경우 편집 영역 닫기
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
alert("언어 키가 영구적으로 삭제되었습니다.");
|
|
||||||
fetchLangKeys(); // 목록 새로고침
|
|
||||||
if (selectedKey && selectedKey.keyId === keyId) {
|
|
||||||
handleCancel(); // 선택된 키가 삭제된 경우 편집 영역 닫기
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
interface Language {
|
interface Language {
|
||||||
langCode: string;
|
langCode: string;
|
||||||
|
|
@ -94,8 +95,8 @@ export function LangKeyModal({
|
||||||
|
|
||||||
const fetchLangTexts = async (keyId: number) => {
|
const fetchLangTexts = async (keyId: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/multilang/keys/${keyId}/texts`);
|
const response = await apiClient.get(`/api/admin/multilang/keys/${keyId}/texts`);
|
||||||
const data = await response.json();
|
const data = response.data;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const texts = data.data;
|
const texts = data.data;
|
||||||
const allTexts = languages.map((lang) => {
|
const allTexts = languages.map((lang) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { setTranslationCache } from "@/lib/utils/multilang";
|
import { setTranslationCache } from "@/lib/utils/multilang";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
interface UseMultiLangOptions {
|
interface UseMultiLangOptions {
|
||||||
companyCode?: string;
|
companyCode?: string;
|
||||||
|
|
@ -35,32 +36,24 @@ export function useMultiLang(options: UseMultiLangOptions = {}) {
|
||||||
const fetchUserLocale = async () => {
|
const fetchUserLocale = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("🔍 사용자 로케일 조회 시작");
|
console.log("🔍 사용자 로케일 조회 시작");
|
||||||
const response = await fetch(`${API_BASE_URL}/admin/user-locale`, {
|
const response = await apiClient.get("/admin/user-locale");
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.data.success && response.data.data) {
|
||||||
const data = await response.json();
|
const userLocale = response.data.data;
|
||||||
if (data.success && data.data) {
|
console.log("✅ 사용자 로케일 조회 성공:", userLocale);
|
||||||
const userLocale = data.data;
|
|
||||||
console.log("✅ 사용자 로케일 조회 성공:", userLocale);
|
|
||||||
|
|
||||||
// 사용자 로케일을 데이터베이스 언어 코드로 매핑
|
// 사용자 로케일을 데이터베이스 언어 코드로 매핑
|
||||||
const langMapping: Record<string, string> = {
|
const langMapping: Record<string, string> = {
|
||||||
ko: "KR",
|
ko: "KR",
|
||||||
en: "US",
|
en: "US",
|
||||||
ja: "JP",
|
ja: "JP",
|
||||||
zh: "CN",
|
zh: "CN",
|
||||||
};
|
};
|
||||||
|
|
||||||
const mappedLang = langMapping[userLocale] || userLocale;
|
const mappedLang = langMapping[userLocale] || userLocale;
|
||||||
console.log("🔄 언어 매핑:", userLocale, "->", mappedLang);
|
console.log("🔄 언어 매핑:", userLocale, "->", mappedLang);
|
||||||
setUserLang(mappedLang);
|
setUserLang(mappedLang);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 호출 실패 시 브라우저 언어 사용
|
// API 호출 실패 시 브라우저 언어 사용
|
||||||
|
|
@ -102,35 +95,21 @@ export function useMultiLang(options: UseMultiLangOptions = {}) {
|
||||||
console.log(`🔍 다국어 텍스트 요청:`, { menuCode, langKey, userLang, companyCode });
|
console.log(`🔍 다국어 텍스트 요청:`, { menuCode, langKey, userLang, companyCode });
|
||||||
|
|
||||||
try {
|
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);
|
console.log(`📡 API 요청 URL:`, url);
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await apiClient.get(url);
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`📡 API 응답 상태:`, response.status, response.statusText);
|
console.log(`📡 API 응답 상태:`, response.status, response.statusText);
|
||||||
|
|
||||||
// HTTP 상태 코드 확인
|
if (response.data.success && response.data.data) {
|
||||||
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) {
|
|
||||||
// 개별 번역 텍스트를 캐시에 저장
|
// 개별 번역 텍스트를 캐시에 저장
|
||||||
const cacheKey = `${menuCode}.${langKey}`;
|
const cacheKey = `${menuCode}.${langKey}`;
|
||||||
const currentCache = (window as any).__TRANSLATION_CACHE || {};
|
const currentCache = (window as any).__TRANSLATION_CACHE || {};
|
||||||
currentCache[cacheKey] = data.data;
|
currentCache[cacheKey] = response.data.data;
|
||||||
(window as any).__TRANSLATION_CACHE = currentCache;
|
(window as any).__TRANSLATION_CACHE = currentCache;
|
||||||
|
|
||||||
return data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 실패 시 fallback 또는 키 반환
|
// 실패 시 fallback 또는 키 반환
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useMultiLang } from "@/hooks/useMultiLang";
|
import { useMultiLang } from "@/hooks/useMultiLang";
|
||||||
|
import { apiClient } from "../api/client";
|
||||||
|
|
||||||
// 메뉴 관리 화면 다국어 키 상수
|
// 메뉴 관리 화면 다국어 키 상수
|
||||||
export const MENU_MANAGEMENT_KEYS = {
|
export const MENU_MANAGEMENT_KEYS = {
|
||||||
|
|
@ -194,18 +195,12 @@ export const getMenuTextSync = (key: string, params?: Record<string, any>): stri
|
||||||
|
|
||||||
// 비동기적으로 번역 로드 (백그라운드에서)
|
// 비동기적으로 번역 로드 (백그라운드에서)
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
|
|
||||||
const companyCode = "*";
|
const companyCode = "*";
|
||||||
|
|
||||||
fetch(`${API_BASE_URL}/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${key}?userLang=${userLang}`, {
|
apiClient
|
||||||
credentials: "include",
|
.get(`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${key}?userLang=${userLang}`)
|
||||||
headers: {
|
.then((response) => {
|
||||||
"Content-Type": "application/json",
|
if (response.data.success && response.data.data) {
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.success && data.data) {
|
|
||||||
// 개별 캐시에 저장
|
// 개별 캐시에 저장
|
||||||
const currentCache = (window as any).__TRANSLATION_CACHE || {};
|
const currentCache = (window as any).__TRANSLATION_CACHE || {};
|
||||||
currentCache[cacheKey] = data.data;
|
currentCache[cacheKey] = data.data;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue