Resolve merge conflicts: Use main branch versions for conflicting files
- Update multilangController.ts to main branch version - Add Windows development environment files from main - Include batch files for Windows development support
This commit is contained in:
parent
e7f9320a78
commit
c78f152f68
|
|
@ -1,496 +1,202 @@
|
||||||
import { Request, Response } from "express";
|
import { Response } from "express";
|
||||||
import { logger } from "../utils/logger";
|
|
||||||
import { AuthenticatedRequest } from "../types/auth";
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
import { MultiLangService } from "../services/multilangService";
|
import { logger } from "../utils/logger";
|
||||||
import {
|
import prisma from "../config/database";
|
||||||
CreateLanguageRequest,
|
|
||||||
UpdateLanguageRequest,
|
// 메모리 캐시 (개발 환경용, 운영에서는 Redis 사용 권장)
|
||||||
CreateLangKeyRequest,
|
const translationCache = new Map<string, any>();
|
||||||
UpdateLangKeyRequest,
|
const CACHE_TTL = 5 * 60 * 1000; // 5분
|
||||||
SaveLangTextsRequest,
|
|
||||||
GetUserTextParams,
|
interface CacheEntry {
|
||||||
BatchTranslationRequest,
|
data: any;
|
||||||
ApiResponse,
|
timestamp: number;
|
||||||
} from "../types/multilang";
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/multilang/languages
|
* GET /api/multilang/batch
|
||||||
* 언어 목록 조회 API
|
* 다국어 텍스트 배치 조회 API - 여러 키를 한번에 조회
|
||||||
*/
|
*/
|
||||||
export const getLanguages = async (
|
export const getBatchTranslations = async (
|
||||||
req: AuthenticatedRequest,
|
req: AuthenticatedRequest,
|
||||||
res: Response
|
res: Response
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
logger.info("언어 목록 조회 요청", { user: req.user });
|
const { companyCode, menuCode, userLang } = req.query;
|
||||||
|
const { langKeys } = req.body; // 배열로 여러 키 전달
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
logger.info("다국어 텍스트 배치 조회 요청", {
|
||||||
const languages = await multiLangService.getLanguages();
|
companyCode,
|
||||||
|
menuCode,
|
||||||
const response: ApiResponse<any[]> = {
|
userLang,
|
||||||
success: true,
|
keyCount: langKeys?.length || 0,
|
||||||
message: "언어 목록 조회 성공",
|
|
||||||
data: languages,
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("언어 목록 조회 실패:", error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "언어 목록 조회 중 오류가 발생했습니다.",
|
|
||||||
error: {
|
|
||||||
code: "LANGUAGE_LIST_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/multilang/languages
|
|
||||||
* 언어 생성 API
|
|
||||||
*/
|
|
||||||
export const createLanguage = async (
|
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res: Response
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const languageData: CreateLanguageRequest = req.body;
|
|
||||||
logger.info("언어 생성 요청", { languageData, user: req.user });
|
|
||||||
|
|
||||||
// 필수 입력값 검증
|
|
||||||
if (
|
|
||||||
!languageData.langCode ||
|
|
||||||
!languageData.langName ||
|
|
||||||
!languageData.langNative
|
|
||||||
) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "언어 코드, 언어명, 원어명은 필수입니다.",
|
|
||||||
error: {
|
|
||||||
code: "MISSING_REQUIRED_FIELDS",
|
|
||||||
details: "langCode, langName, langNative are required",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
|
||||||
const createdLanguage = await multiLangService.createLanguage({
|
|
||||||
...languageData,
|
|
||||||
createdBy: req.user?.userId || "system",
|
|
||||||
updatedBy: req.user?.userId || "system",
|
|
||||||
});
|
|
||||||
|
|
||||||
const response: ApiResponse<any> = {
|
|
||||||
success: true,
|
|
||||||
message: "언어가 성공적으로 생성되었습니다.",
|
|
||||||
data: createdLanguage,
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(201).json(response);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("언어 생성 실패:", error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "언어 생성 중 오류가 발생했습니다.",
|
|
||||||
error: {
|
|
||||||
code: "LANGUAGE_CREATE_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PUT /api/multilang/languages/:langCode
|
|
||||||
* 언어 수정 API
|
|
||||||
*/
|
|
||||||
export const updateLanguage = async (
|
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res: Response
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { langCode } = req.params;
|
|
||||||
const languageData: UpdateLanguageRequest = req.body;
|
|
||||||
|
|
||||||
logger.info("언어 수정 요청", { langCode, languageData, user: req.user });
|
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
|
||||||
const updatedLanguage = await multiLangService.updateLanguage(langCode, {
|
|
||||||
...languageData,
|
|
||||||
updatedBy: req.user?.userId || "system",
|
|
||||||
});
|
|
||||||
|
|
||||||
const response: ApiResponse<any> = {
|
|
||||||
success: true,
|
|
||||||
message: "언어가 성공적으로 수정되었습니다.",
|
|
||||||
data: updatedLanguage,
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("언어 수정 실패:", error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "언어 수정 중 오류가 발생했습니다.",
|
|
||||||
error: {
|
|
||||||
code: "LANGUAGE_UPDATE_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PUT /api/multilang/languages/:langCode/toggle
|
|
||||||
* 언어 상태 토글 API
|
|
||||||
*/
|
|
||||||
export const toggleLanguage = async (
|
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res: Response
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { langCode } = req.params;
|
|
||||||
logger.info("언어 상태 토글 요청", { langCode, user: req.user });
|
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
|
||||||
const result = await multiLangService.toggleLanguage(langCode);
|
|
||||||
|
|
||||||
const response: ApiResponse<string> = {
|
|
||||||
success: true,
|
|
||||||
message: `언어가 ${result}되었습니다.`,
|
|
||||||
data: result,
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("언어 상태 토글 실패:", error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "언어 상태 변경 중 오류가 발생했습니다.",
|
|
||||||
error: {
|
|
||||||
code: "LANGUAGE_TOGGLE_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/multilang/keys
|
|
||||||
* 다국어 키 목록 조회 API
|
|
||||||
*/
|
|
||||||
export const getLangKeys = async (
|
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res: Response
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { companyCode, menuCode, keyType, searchText } = req.query;
|
|
||||||
logger.info("다국어 키 목록 조회 요청", {
|
|
||||||
query: req.query,
|
|
||||||
user: req.user,
|
user: req.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
if (!langKeys || !Array.isArray(langKeys) || langKeys.length === 0) {
|
||||||
const langKeys = await multiLangService.getLangKeys({
|
|
||||||
companyCode: companyCode as string,
|
|
||||||
menuCode: menuCode as string,
|
|
||||||
keyType: keyType as string,
|
|
||||||
searchText: searchText as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
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: {
|
|
||||||
code: "LANG_KEYS_LIST_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/multilang/keys/:keyId/texts
|
|
||||||
* 특정 키의 다국어 텍스트 조회 API
|
|
||||||
*/
|
|
||||||
export const getLangTexts = async (
|
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res: Response
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { keyId } = req.params;
|
|
||||||
logger.info("다국어 텍스트 조회 요청", { keyId, user: req.user });
|
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
|
||||||
const langTexts = await multiLangService.getLangTexts(parseInt(keyId));
|
|
||||||
|
|
||||||
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: {
|
|
||||||
code: "LANG_TEXTS_LIST_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/multilang/keys
|
|
||||||
* 다국어 키 생성 API
|
|
||||||
*/
|
|
||||||
export const createLangKey = async (
|
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res: Response
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const keyData: CreateLangKeyRequest = req.body;
|
|
||||||
logger.info("다국어 키 생성 요청", { keyData, user: req.user });
|
|
||||||
|
|
||||||
// 필수 입력값 검증
|
|
||||||
if (!keyData.companyCode || !keyData.langKey) {
|
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "회사 코드와 언어 키는 필수입니다.",
|
message: "langKeys 배열이 필요합니다.",
|
||||||
error: {
|
|
||||||
code: "MISSING_REQUIRED_FIELDS",
|
|
||||||
details: "companyCode and langKey are required",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
// 캐시 키 생성
|
||||||
const keyId = await multiLangService.createLangKey({
|
const cacheKey = `${companyCode}_${userLang}_${langKeys.sort().join("_")}`;
|
||||||
...keyData,
|
|
||||||
createdBy: req.user?.userId || "system",
|
// 캐시 확인
|
||||||
updatedBy: req.user?.userId || "system",
|
const cached = translationCache.get(cacheKey);
|
||||||
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||||
|
logger.info("캐시된 번역 데이터 사용", {
|
||||||
|
cacheKey,
|
||||||
|
keyCount: langKeys.length,
|
||||||
|
});
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: cached.data,
|
||||||
|
message: "캐시된 다국어 텍스트 조회 성공",
|
||||||
|
fromCache: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 모든 키에 대한 마스터 정보를 한번에 조회
|
||||||
|
logger.info("다국어 키 마스터 배치 조회 시작", {
|
||||||
|
keyCount: langKeys.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: ApiResponse<number> = {
|
const langKeyMasters = await prisma.$queryRaw<any[]>`
|
||||||
success: true,
|
SELECT key_id, lang_key, company_code
|
||||||
message: "다국어 키가 성공적으로 생성되었습니다.",
|
FROM multi_lang_key_master
|
||||||
data: keyId,
|
WHERE lang_key = ANY(${langKeys}::varchar[])
|
||||||
};
|
AND (company_code = ${companyCode}::varchar OR company_code = '*')
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN company_code = ${companyCode}::varchar THEN 1 ELSE 2 END,
|
||||||
|
lang_key,
|
||||||
|
company_code
|
||||||
|
`;
|
||||||
|
|
||||||
res.status(201).json(response);
|
logger.info("다국어 키 마스터 배치 조회 결과", {
|
||||||
} catch (error) {
|
requestedKeys: langKeys.length,
|
||||||
logger.error("다국어 키 생성 실패:", error);
|
foundKeys: langKeyMasters.length,
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "다국어 키 생성 중 오류가 발생했습니다.",
|
|
||||||
error: {
|
|
||||||
code: "LANG_KEY_CREATE_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PUT /api/multilang/keys/:keyId
|
|
||||||
* 다국어 키 수정 API
|
|
||||||
*/
|
|
||||||
export const updateLangKey = async (
|
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res: Response
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { keyId } = req.params;
|
|
||||||
const keyData: UpdateLangKeyRequest = req.body;
|
|
||||||
|
|
||||||
logger.info("다국어 키 수정 요청", { keyId, keyData, user: req.user });
|
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
|
||||||
await multiLangService.updateLangKey(parseInt(keyId), {
|
|
||||||
...keyData,
|
|
||||||
updatedBy: req.user?.userId || "system",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: ApiResponse<string> = {
|
if (langKeyMasters.length === 0) {
|
||||||
success: true,
|
// 마스터 데이터가 없으면 기본값 반환
|
||||||
message: "다국어 키가 성공적으로 수정되었습니다.",
|
const defaultTranslations = getDefaultTranslations(
|
||||||
data: "수정 완료",
|
langKeys,
|
||||||
};
|
userLang as string
|
||||||
|
);
|
||||||
|
|
||||||
res.status(200).json(response);
|
// 캐시에 저장
|
||||||
} catch (error) {
|
translationCache.set(cacheKey, {
|
||||||
logger.error("다국어 키 수정 실패:", error);
|
data: defaultTranslations,
|
||||||
res.status(500).json({
|
timestamp: Date.now(),
|
||||||
success: false,
|
});
|
||||||
message: "다국어 키 수정 중 오류가 발생했습니다.",
|
|
||||||
error: {
|
res.status(200).json({
|
||||||
code: "LANG_KEY_UPDATE_ERROR",
|
success: true,
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
data: defaultTranslations,
|
||||||
},
|
message: "기본값으로 다국어 텍스트 조회 성공",
|
||||||
|
fromCache: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 모든 key_id를 추출
|
||||||
|
const keyIds = langKeyMasters.map((master) => master.key_id);
|
||||||
|
|
||||||
|
// 3. 요청된 언어와 한국어 번역을 한번에 조회
|
||||||
|
const translations = await prisma.$queryRaw<any[]>`
|
||||||
|
SELECT
|
||||||
|
mlt.key_id,
|
||||||
|
mlt.lang_code,
|
||||||
|
mlt.lang_text,
|
||||||
|
mlkm.lang_key
|
||||||
|
FROM multi_lang_text mlt
|
||||||
|
JOIN multi_lang_key_master mlkm ON mlt.key_id = mlkm.key_id
|
||||||
|
WHERE mlt.key_id = ANY(${keyIds}::numeric[])
|
||||||
|
AND mlt.lang_code IN (${userLang}::varchar, 'KR')
|
||||||
|
ORDER BY
|
||||||
|
mlt.key_id,
|
||||||
|
CASE WHEN mlt.lang_code = ${userLang}::varchar THEN 1 ELSE 2 END
|
||||||
|
`;
|
||||||
|
|
||||||
|
logger.info("번역 텍스트 배치 조회 결과", {
|
||||||
|
keyIds: keyIds.length,
|
||||||
|
translations: translations.length,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
// 4. 결과를 키별로 정리
|
||||||
* DELETE /api/multilang/keys/:keyId
|
const result: Record<string, string> = {};
|
||||||
* 다국어 키 삭제 API
|
|
||||||
*/
|
|
||||||
export const deleteLangKey = async (
|
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res: Response
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { keyId } = req.params;
|
|
||||||
logger.info("다국어 키 삭제 요청", { keyId, user: req.user });
|
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
for (const langKey of langKeys) {
|
||||||
await multiLangService.deleteLangKey(parseInt(keyId));
|
const master = langKeyMasters.find((m) => m.lang_key === langKey);
|
||||||
|
|
||||||
const response: ApiResponse<string> = {
|
if (master) {
|
||||||
success: true,
|
const keyId = master.key_id;
|
||||||
message: "다국어 키가 성공적으로 삭제되었습니다.",
|
|
||||||
data: "삭제 완료",
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(200).json(response);
|
// 요청된 언어 번역 찾기
|
||||||
} catch (error) {
|
let translation = translations.find(
|
||||||
logger.error("다국어 키 삭제 실패:", error);
|
(t) => t.key_id === keyId && t.lang_code === userLang
|
||||||
res.status(500).json({
|
);
|
||||||
success: false,
|
|
||||||
message: "다국어 키 삭제 중 오류가 발생했습니다.",
|
|
||||||
error: {
|
|
||||||
code: "LANG_KEY_DELETE_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
// 요청된 언어가 없으면 한국어 번역 찾기
|
||||||
* PUT /api/multilang/keys/:keyId/toggle
|
if (!translation) {
|
||||||
* 다국어 키 상태 토글 API
|
translation = translations.find(
|
||||||
*/
|
(t) => t.key_id === keyId && t.lang_code === "KR"
|
||||||
export const toggleLangKey = async (
|
);
|
||||||
req: AuthenticatedRequest,
|
}
|
||||||
res: Response
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { keyId } = req.params;
|
|
||||||
logger.info("다국어 키 상태 토글 요청", { keyId, user: req.user });
|
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
// 번역이 있으면 사용, 없으면 기본값
|
||||||
const result = await multiLangService.toggleLangKey(parseInt(keyId));
|
if (translation) {
|
||||||
|
result[langKey] = translation.lang_text;
|
||||||
|
} else {
|
||||||
|
result[langKey] = getDefaultTranslation(langKey, userLang as string);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 마스터 데이터가 없으면 기본값
|
||||||
|
result[langKey] = getDefaultTranslation(langKey, userLang as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response: ApiResponse<string> = {
|
// 5. 캐시에 저장
|
||||||
success: true,
|
translationCache.set(cacheKey, {
|
||||||
message: `다국어 키가 ${result}되었습니다.`,
|
|
||||||
data: result,
|
data: result,
|
||||||
};
|
timestamp: Date.now(),
|
||||||
|
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("다국어 키 상태 토글 실패:", error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "다국어 키 상태 변경 중 오류가 발생했습니다.",
|
|
||||||
error: {
|
|
||||||
code: "LANG_KEY_TOGGLE_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/multilang/keys/:keyId/texts
|
|
||||||
* 다국어 텍스트 저장/수정 API
|
|
||||||
*/
|
|
||||||
export const saveLangTexts = async (
|
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res: Response
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { keyId } = req.params;
|
|
||||||
const textData: SaveLangTextsRequest = req.body;
|
|
||||||
|
|
||||||
logger.info("다국어 텍스트 저장 요청", { keyId, textData, user: req.user });
|
|
||||||
|
|
||||||
// 필수 입력값 검증
|
|
||||||
if (
|
|
||||||
!textData.texts ||
|
|
||||||
!Array.isArray(textData.texts) ||
|
|
||||||
textData.texts.length === 0
|
|
||||||
) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "텍스트 데이터는 필수입니다.",
|
|
||||||
error: {
|
|
||||||
code: "MISSING_REQUIRED_FIELDS",
|
|
||||||
details: "texts array is required",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
|
||||||
await multiLangService.saveLangTexts(parseInt(keyId), {
|
|
||||||
texts: textData.texts.map((text) => ({
|
|
||||||
...text,
|
|
||||||
createdBy: req.user?.userId || "system",
|
|
||||||
updatedBy: req.user?.userId || "system",
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: ApiResponse<string> = {
|
logger.info("다국어 텍스트 배치 조회 완료", {
|
||||||
|
requestedKeys: langKeys.length,
|
||||||
|
resultKeys: Object.keys(result).length,
|
||||||
|
cacheKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "다국어 텍스트가 성공적으로 저장되었습니다.",
|
data: result,
|
||||||
data: "저장 완료",
|
message: "다국어 텍스트 배치 조회 성공",
|
||||||
};
|
fromCache: false,
|
||||||
|
});
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("다국어 텍스트 저장 실패:", error);
|
logger.error("다국어 텍스트 배치 조회 실패", { error });
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "다국어 텍스트 저장 중 오류가 발생했습니다.",
|
message: "다국어 텍스트 배치 조회 중 오류가 발생했습니다.",
|
||||||
error: {
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
code: "LANG_TEXTS_SAVE_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/multilang/user-text/:companyCode/:menuCode/:langKey
|
* GET /api/multilang/user-text/:companyCode/:menuCode/:langKey
|
||||||
* 사용자별 다국어 텍스트 조회 API
|
* 단일 다국어 텍스트 조회 API (하위 호환성 유지)
|
||||||
*/
|
*/
|
||||||
export const getUserText = async (
|
export const getUserText = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res: Response
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
try {
|
||||||
const { companyCode, menuCode, langKey } = req.params;
|
const { companyCode, menuCode, langKey } = req.params;
|
||||||
const { userLang } = req.query;
|
const { userLang } = req.query;
|
||||||
|
|
||||||
logger.info("사용자별 다국어 텍스트 조회 요청", {
|
logger.info("단일 다국어 텍스트 조회 요청", {
|
||||||
companyCode,
|
companyCode,
|
||||||
menuCode,
|
menuCode,
|
||||||
langKey,
|
langKey,
|
||||||
|
|
@ -498,215 +204,122 @@ export const getUserText = async (
|
||||||
user: req.user,
|
user: req.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userLang) {
|
// 배치 API를 사용하여 단일 키 조회
|
||||||
res.status(400).json({
|
const batchResult = await getBatchTranslations(
|
||||||
success: false,
|
{
|
||||||
message: "사용자 언어는 필수입니다.",
|
...req,
|
||||||
error: {
|
body: { langKeys: [langKey] },
|
||||||
code: "MISSING_USER_LANG",
|
query: { companyCode, menuCode, userLang },
|
||||||
details: "userLang query parameter is required",
|
} as any,
|
||||||
},
|
res
|
||||||
});
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
// 배치 API에서 이미 응답을 보냈으므로 여기서는 아무것도 하지 않음
|
||||||
const langText = await multiLangService.getUserText({
|
return;
|
||||||
companyCode,
|
|
||||||
menuCode,
|
|
||||||
langKey,
|
|
||||||
userLang: userLang as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response: ApiResponse<string> = {
|
|
||||||
success: true,
|
|
||||||
message: "사용자별 다국어 텍스트 조회 성공",
|
|
||||||
data: langText,
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("사용자별 다국어 텍스트 조회 실패:", error);
|
logger.error("단일 다국어 텍스트 조회 실패", { error });
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "사용자별 다국어 텍스트 조회 중 오류가 발생했습니다.",
|
message: "다국어 텍스트 조회 중 오류가 발생했습니다.",
|
||||||
error: {
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
code: "USER_TEXT_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/multilang/text/:companyCode/:langKey/:langCode
|
* 기본 번역 텍스트 반환 (개별 키)
|
||||||
* 특정 키의 다국어 텍스트 조회 API
|
|
||||||
*/
|
*/
|
||||||
export const getLangText = async (
|
function getDefaultTranslation(langKey: string, userLang: string): string {
|
||||||
req: AuthenticatedRequest,
|
const defaultKoreanTexts: Record<string, string> = {
|
||||||
res: Response
|
"button.add": "추가",
|
||||||
): Promise<void> => {
|
"button.add.top.level": "최상위 메뉴 추가",
|
||||||
try {
|
"button.add.sub": "하위 메뉴 추가",
|
||||||
const { companyCode, langKey, langCode } = req.params;
|
"button.edit": "수정",
|
||||||
|
"button.delete": "삭제",
|
||||||
|
"button.cancel": "취소",
|
||||||
|
"button.save": "저장",
|
||||||
|
"button.register": "등록",
|
||||||
|
"form.menu.name": "메뉴명",
|
||||||
|
"form.menu.url": "URL",
|
||||||
|
"form.menu.description": "설명",
|
||||||
|
"form.menu.type": "메뉴 타입",
|
||||||
|
"form.status": "상태",
|
||||||
|
"form.company": "회사",
|
||||||
|
"table.header.menu.name": "메뉴명",
|
||||||
|
"table.header.menu.url": "URL",
|
||||||
|
"table.header.status": "상태",
|
||||||
|
"table.header.company": "회사",
|
||||||
|
"table.header.actions": "작업",
|
||||||
|
"filter.company": "회사",
|
||||||
|
"filter.search": "검색",
|
||||||
|
"filter.reset": "초기화",
|
||||||
|
"menu.type.title": "메뉴 타입",
|
||||||
|
"menu.type.admin": "관리자",
|
||||||
|
"menu.type.user": "사용자",
|
||||||
|
"status.active": "활성화",
|
||||||
|
"status.inactive": "비활성화",
|
||||||
|
"form.lang.key": "언어 키",
|
||||||
|
"form.lang.key.select": "언어 키 선택",
|
||||||
|
"form.menu.name.placeholder": "메뉴명을 입력하세요",
|
||||||
|
"form.menu.url.placeholder": "URL을 입력하세요",
|
||||||
|
"form.menu.description.placeholder": "설명을 입력하세요",
|
||||||
|
"form.menu.sequence": "순서",
|
||||||
|
"form.menu.sequence.placeholder": "순서를 입력하세요",
|
||||||
|
"form.status.active": "활성",
|
||||||
|
"form.status.inactive": "비활성",
|
||||||
|
"form.company.select": "회사 선택",
|
||||||
|
"form.company.common": "공통",
|
||||||
|
"form.company.submenu.note": "하위메뉴는 회사별로 관리됩니다",
|
||||||
|
"filter.company.common": "공통",
|
||||||
|
"filter.search.placeholder": "검색어를 입력하세요",
|
||||||
|
"modal.menu.register.title": "메뉴 등록",
|
||||||
|
};
|
||||||
|
|
||||||
logger.info("특정 키의 다국어 텍스트 조회 요청", {
|
return defaultKoreanTexts[langKey] || langKey;
|
||||||
companyCode,
|
}
|
||||||
langKey,
|
|
||||||
langCode,
|
/**
|
||||||
|
* 기본 번역 텍스트 반환 (배치)
|
||||||
|
*/
|
||||||
|
function getDefaultTranslations(
|
||||||
|
langKeys: string[],
|
||||||
|
userLang: string
|
||||||
|
): Record<string, string> {
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const langKey of langKeys) {
|
||||||
|
result[langKey] = getDefaultTranslation(langKey, userLang);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 캐시 초기화 (개발/테스트용)
|
||||||
|
*/
|
||||||
|
export const clearCache = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const beforeSize = translationCache.size;
|
||||||
|
translationCache.clear();
|
||||||
|
|
||||||
|
logger.info("다국어 캐시 초기화 완료", {
|
||||||
|
beforeSize,
|
||||||
|
afterSize: 0,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
res.status(200).json({
|
||||||
const langText = await multiLangService.getLangText(
|
|
||||||
companyCode,
|
|
||||||
langKey,
|
|
||||||
langCode
|
|
||||||
);
|
|
||||||
|
|
||||||
const response: ApiResponse<string> = {
|
|
||||||
success: true,
|
success: true,
|
||||||
message: "특정 키의 다국어 텍스트 조회 성공",
|
message: "캐시가 초기화되었습니다.",
|
||||||
data: langText,
|
beforeSize,
|
||||||
};
|
afterSize: 0,
|
||||||
|
});
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("특정 키의 다국어 텍스트 조회 실패:", error);
|
logger.error("캐시 초기화 실패", { error });
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "특정 키의 다국어 텍스트 조회 중 오류가 발생했습니다.",
|
message: "캐시 초기화 중 오류가 발생했습니다.",
|
||||||
error: {
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
code: "LANG_TEXT_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE /api/multilang/languages/:langCode
|
|
||||||
* 언어 삭제 API
|
|
||||||
*/
|
|
||||||
export const deleteLanguage = async (
|
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res: Response
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { langCode } = req.params;
|
|
||||||
logger.info("언어 삭제 요청", { langCode, user: req.user });
|
|
||||||
|
|
||||||
if (!langCode) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "언어 코드가 필요합니다.",
|
|
||||||
error: {
|
|
||||||
code: "MISSING_LANG_CODE",
|
|
||||||
details: "langCode parameter is required",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
|
||||||
await multiLangService.deleteLanguage(langCode);
|
|
||||||
|
|
||||||
const response: ApiResponse<string> = {
|
|
||||||
success: true,
|
|
||||||
message: "언어가 성공적으로 삭제되었습니다.",
|
|
||||||
data: "삭제 완료",
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("언어 삭제 실패:", error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "언어 삭제 중 오류가 발생했습니다.",
|
|
||||||
error: {
|
|
||||||
code: "LANGUAGE_DELETE_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/multilang/batch
|
|
||||||
* 다국어 텍스트 배치 조회 API
|
|
||||||
*/
|
|
||||||
export const getBatchTranslations = async (
|
|
||||||
req: Request,
|
|
||||||
res: Response
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { companyCode, menuCode, userLang } = req.query;
|
|
||||||
const {
|
|
||||||
langKeys,
|
|
||||||
companyCode: bodyCompanyCode,
|
|
||||||
menuCode: bodyMenuCode,
|
|
||||||
userLang: bodyUserLang,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// query params에서 읽지 못한 경우 body에서 읽기
|
|
||||||
const finalCompanyCode = companyCode || bodyCompanyCode;
|
|
||||||
const finalMenuCode = menuCode || bodyMenuCode;
|
|
||||||
const finalUserLang = userLang || bodyUserLang;
|
|
||||||
|
|
||||||
logger.info("다국어 텍스트 배치 조회 요청", {
|
|
||||||
companyCode: finalCompanyCode,
|
|
||||||
menuCode: finalMenuCode,
|
|
||||||
userLang: finalUserLang,
|
|
||||||
keyCount: langKeys?.length || 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!langKeys || !Array.isArray(langKeys) || langKeys.length === 0) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "langKeys 배열이 필요합니다.",
|
|
||||||
error: {
|
|
||||||
code: "MISSING_LANG_KEYS",
|
|
||||||
details: "langKeys array is required",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!finalCompanyCode || !finalUserLang) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "companyCode와 userLang은 필수입니다.",
|
|
||||||
error: {
|
|
||||||
code: "MISSING_REQUIRED_PARAMS",
|
|
||||||
details: "companyCode and userLang are required",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
|
||||||
const translations = await multiLangService.getBatchTranslations({
|
|
||||||
companyCode: finalCompanyCode as string,
|
|
||||||
menuCode: finalMenuCode as string,
|
|
||||||
userLang: finalUserLang as string,
|
|
||||||
langKeys,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response: ApiResponse<Record<string, string>> = {
|
|
||||||
success: true,
|
|
||||||
message: "다국어 텍스트 배치 조회 성공",
|
|
||||||
data: translations,
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("다국어 텍스트 배치 조회 실패:", error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "다국어 텍스트 배치 조회 중 오류가 발생했습니다.",
|
|
||||||
error: {
|
|
||||||
code: "BATCH_TRANSLATION_ERROR",
|
|
||||||
details: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Node.js 백엔드
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend-node
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: pms-backend-win
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- PORT=8080
|
||||||
|
- DATABASE_URL=postgresql://postgres:ph0909!!@39.117.244.52:11132/plm
|
||||||
|
- JWT_SECRET=ilshin-plm-super-secret-jwt-key-2024
|
||||||
|
- JWT_EXPIRES_IN=24h
|
||||||
|
- CORS_ORIGIN=http://localhost:9771
|
||||||
|
- CORS_CREDENTIALS=true
|
||||||
|
- LOG_LEVEL=debug
|
||||||
|
volumes:
|
||||||
|
- ./backend-node:/app
|
||||||
|
- /app/node_modules
|
||||||
|
networks:
|
||||||
|
- pms-network
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
pms-network:
|
||||||
|
driver: bridge
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Next.js 프론트엔드만
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
container_name: pms-frontend-win
|
||||||
|
ports:
|
||||||
|
- "9771:3000"
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_API_URL=http://localhost:8080/api
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/app
|
||||||
|
- /app/node_modules
|
||||||
|
- /app/.next
|
||||||
|
networks:
|
||||||
|
- pms-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
pms-network:
|
||||||
|
driver: bridge
|
||||||
|
external: true
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
services:
|
||||||
|
# 백엔드 서비스 (기존)
|
||||||
|
plm-app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.win
|
||||||
|
platforms:
|
||||||
|
- linux/amd64
|
||||||
|
container_name: plm-windows
|
||||||
|
ports:
|
||||||
|
- "9090:8080"
|
||||||
|
environment:
|
||||||
|
- CATALINA_OPTS=-DDB_URL=jdbc:postgresql://39.117.244.52:11132/plm -DDB_USERNAME=postgres -DDB_PASSWORD=ph0909!! -Xms512m -Xmx1024m
|
||||||
|
- TZ=Asia/Seoul
|
||||||
|
- APP_ENV=development
|
||||||
|
- LOG_LEVEL=INFO
|
||||||
|
- DB_HOST=39.117.244.52
|
||||||
|
- DB_PORT=11132
|
||||||
|
- DB_NAME=plm
|
||||||
|
- DB_USERNAME=postgres
|
||||||
|
- DB_PASSWORD=ph0909!!
|
||||||
|
volumes:
|
||||||
|
- plm-win-project:/data_storage
|
||||||
|
- plm-win-app:/app_data
|
||||||
|
- ./logs:/usr/local/tomcat/logs
|
||||||
|
- ./WebContent:/usr/local/tomcat/webapps/ROOT
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- plm-network
|
||||||
|
|
||||||
|
# 프론트엔드 서비스 (새로 추가)
|
||||||
|
plm-frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
container_name: plm-frontend
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- TZ=Asia/Seoul
|
||||||
|
- NEXT_PUBLIC_API_URL=http://localhost:9090
|
||||||
|
- WATCHPACK_POLLING=true
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/app
|
||||||
|
- /app/node_modules
|
||||||
|
- /app/.next
|
||||||
|
depends_on:
|
||||||
|
- plm-app
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- plm-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
plm-win-project:
|
||||||
|
driver: local
|
||||||
|
plm-win-app:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
plm-network:
|
||||||
|
driver: bridge
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
echo =====================================
|
||||||
|
echo PLM 솔루션 - Windows 시작
|
||||||
|
echo =====================================
|
||||||
|
|
||||||
|
echo 기존 컨테이너 정리 중...
|
||||||
|
docker-compose -f docker-compose.win.yml down 2>nul
|
||||||
|
|
||||||
|
echo PLM 서비스 시작 중...
|
||||||
|
docker-compose -f docker-compose.win.yml up --build --force-recreate -d
|
||||||
|
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo.
|
||||||
|
echo ✅ PLM 서비스가 성공적으로 시작되었습니다!
|
||||||
|
echo.
|
||||||
|
echo 🌐 접속 URL:
|
||||||
|
echo • 프론트엔드 (Next.js): http://localhost:3000
|
||||||
|
echo • 백엔드 (Spring/JSP): http://localhost:9090
|
||||||
|
echo.
|
||||||
|
echo 📋 서비스 상태 확인:
|
||||||
|
echo docker-compose -f docker-compose.win.yml ps
|
||||||
|
echo.
|
||||||
|
echo 📊 로그 확인:
|
||||||
|
echo docker-compose -f docker-compose.win.yml logs
|
||||||
|
echo.
|
||||||
|
echo 5초 후 프론트엔드 페이지를 자동으로 엽니다...
|
||||||
|
timeout /t 5 /nobreak >nul
|
||||||
|
start http://localhost:3000
|
||||||
|
) else (
|
||||||
|
echo.
|
||||||
|
echo ❌ PLM 서비스 시작에 실패했습니다!
|
||||||
|
echo.
|
||||||
|
echo 🔍 문제 해결 방법:
|
||||||
|
echo 1. Docker Desktop이 실행 중인지 확인
|
||||||
|
echo 2. 포트가 사용 중인지 확인 (3000, 9090)
|
||||||
|
echo 3. 로그 확인: docker-compose -f docker-compose.win.yml logs
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
|
||||||
|
echo ============================================
|
||||||
|
echo PLM 솔루션 - 전체 서비스 시작 (분리형)
|
||||||
|
echo ============================================
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 🚀 백엔드와 프론트엔드를 순차적으로 시작합니다...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM 백엔드 먼저 시작
|
||||||
|
echo ============================================
|
||||||
|
echo 1. 백엔드 서비스 시작 중...
|
||||||
|
echo ============================================
|
||||||
|
|
||||||
|
docker-compose -f docker-compose.backend.win.yml build --no-cache
|
||||||
|
docker-compose -f docker-compose.backend.win.yml down -v
|
||||||
|
docker network create pms-network 2>nul || echo 네트워크가 이미 존재합니다.
|
||||||
|
docker-compose -f docker-compose.backend.win.yml up -d
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ⏳ 백엔드 서비스 안정화 대기 중... (20초)
|
||||||
|
timeout /t 20 /nobreak >nul
|
||||||
|
|
||||||
|
REM 프론트엔드 시작
|
||||||
|
echo.
|
||||||
|
echo ============================================
|
||||||
|
echo 2. 프론트엔드 서비스 시작 중...
|
||||||
|
echo ============================================
|
||||||
|
|
||||||
|
docker-compose -f docker-compose.frontend.win.yml build --no-cache
|
||||||
|
docker-compose -f docker-compose.frontend.win.yml down -v
|
||||||
|
docker-compose -f docker-compose.frontend.win.yml up -d
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ⏳ 프론트엔드 서비스 안정화 대기 중... (10초)
|
||||||
|
timeout /t 10 /nobreak >nul
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ============================================
|
||||||
|
echo 🎉 모든 서비스가 시작되었습니다!
|
||||||
|
echo ============================================
|
||||||
|
echo.
|
||||||
|
echo [DATABASE] PostgreSQL: http://39.117.244.52:11132
|
||||||
|
echo [BACKEND] Spring Boot: http://localhost:8080/api
|
||||||
|
echo [FRONTEND] Next.js: http://localhost:9771
|
||||||
|
echo.
|
||||||
|
echo 서비스 상태 확인:
|
||||||
|
echo 백엔드: docker-compose -f docker-compose.backend.win.yml ps
|
||||||
|
echo 프론트엔드: docker-compose -f docker-compose.frontend.win.yml ps
|
||||||
|
echo.
|
||||||
|
echo 로그 확인:
|
||||||
|
echo 백엔드: docker-compose -f docker-compose.backend.win.yml logs -f
|
||||||
|
echo 프론트엔드: docker-compose -f docker-compose.frontend.win.yml logs -f
|
||||||
|
echo.
|
||||||
|
echo 서비스 중지:
|
||||||
|
echo 백엔드: docker-compose -f docker-compose.backend.win.yml down
|
||||||
|
echo 프론트엔드: docker-compose -f docker-compose.frontend.win.yml down
|
||||||
|
echo 전체: stop-all-separated.bat
|
||||||
|
echo.
|
||||||
|
echo ============================================
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
@ -0,0 +1,472 @@
|
||||||
|
# 테이블 타입 관리 개선 계획서
|
||||||
|
|
||||||
|
## 🎯 개선 목표
|
||||||
|
|
||||||
|
현재 테이블 타입 관리 시스템의 용어 통일과 타입 단순화를 통해 사용자 친화적이고 유연한 시스템으로 개선합니다.
|
||||||
|
|
||||||
|
## 📋 주요 변경사항
|
||||||
|
|
||||||
|
### 1. 용어 변경
|
||||||
|
|
||||||
|
- **웹 타입(Web Type)** → **입력 타입(Input Type)**
|
||||||
|
- 사용자에게 더 직관적인 명칭으로 변경
|
||||||
|
|
||||||
|
### 2. 입력 타입 단순화
|
||||||
|
|
||||||
|
기존 20개 타입에서 **8개 핵심 타입**으로 단순화:
|
||||||
|
|
||||||
|
| 번호 | 입력 타입 | 설명 | 예시 |
|
||||||
|
| ---- | ---------- | ---------- | -------------------- |
|
||||||
|
| 1 | `text` | 텍스트 | 이름, 제목, 설명 |
|
||||||
|
| 2 | `number` | 숫자 | 수량, 건수, 순번 |
|
||||||
|
| 3 | `date` | 날짜 | 생성일, 완료일, 기한 |
|
||||||
|
| 4 | `code` | 코드 | 상태코드, 유형코드 |
|
||||||
|
| 5 | `entity` | 엔티티 | 고객선택, 제품선택 |
|
||||||
|
| 6 | `select` | 선택박스 | 드롭다운 목록 |
|
||||||
|
| 7 | `checkbox` | 체크박스 | Y/N, 사용여부 |
|
||||||
|
| 8 | `radio` | 라디오버튼 | 단일선택 옵션 |
|
||||||
|
|
||||||
|
### 3. DB 타입 제거
|
||||||
|
|
||||||
|
- 컬럼 정보에서 `dbType` 필드 제거
|
||||||
|
- 입력 타입만으로 데이터 처리 방식 결정
|
||||||
|
|
||||||
|
## 🛠️ 기술적 구현 방안
|
||||||
|
|
||||||
|
### 전체 VARCHAR 통일 방식 (확정)
|
||||||
|
|
||||||
|
**모든 신규 테이블의 사용자 정의 컬럼을 VARCHAR(500)로 생성하고 애플리케이션 레벨에서 형변환 처리**
|
||||||
|
|
||||||
|
#### 핵심 장점
|
||||||
|
|
||||||
|
- ✅ **최대 유연성**: 어떤 데이터든 타입 에러 없이 저장 가능
|
||||||
|
- ✅ **에러 제로**: 타입 불일치로 인한 DB 삽입/수정 에러 완전 차단
|
||||||
|
- ✅ **개발 속도**: 복잡한 타입 고민 없이 빠른 개발 가능
|
||||||
|
- ✅ **요구사항 대응**: 필드 성격 변경 시에도 스키마 수정 불필요
|
||||||
|
- ✅ **데이터 마이그레이션**: 기존 시스템 데이터 이관 시 100% 안전
|
||||||
|
|
||||||
|
#### 실제 예시
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 기존 방식 (타입별 생성)
|
||||||
|
CREATE TABLE products (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
name varchar(255), -- 텍스트
|
||||||
|
price numeric(10,2), -- 숫자
|
||||||
|
launch_date date, -- 날짜
|
||||||
|
is_active boolean -- 체크박스
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 새로운 방식 (VARCHAR 통일)
|
||||||
|
CREATE TABLE products (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
created_date timestamp DEFAULT now(),
|
||||||
|
updated_date timestamp DEFAULT now(),
|
||||||
|
company_code varchar(50) DEFAULT '*',
|
||||||
|
writer varchar(100),
|
||||||
|
-- 사용자 정의 컬럼들은 모두 VARCHAR(500)
|
||||||
|
name varchar(500), -- 입력타입: text
|
||||||
|
price varchar(500), -- 입력타입: number → "15000.50"
|
||||||
|
launch_date varchar(500), -- 입력타입: date → "2024-03-15"
|
||||||
|
is_active varchar(500) -- 입력타입: checkbox → "Y"/"N"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 애플리케이션 레벨 처리
|
||||||
|
|
||||||
|
````typescript
|
||||||
|
// 입력 타입별 형변환 및 검증
|
||||||
|
export class InputTypeProcessor {
|
||||||
|
|
||||||
|
// 저장 전 변환 (화면 → DB)
|
||||||
|
static convertForStorage(value: any, inputType: string): string {
|
||||||
|
if (value === null || value === undefined) return "";
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case "text":
|
||||||
|
return String(value);
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
const num = parseFloat(String(value));
|
||||||
|
return isNaN(num) ? "0" : String(num);
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
if (!value) return "";
|
||||||
|
const date = new Date(value);
|
||||||
|
return isNaN(date.getTime()) ? "" : date.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
return ["true", "1", "Y", "yes"].includes(String(value).toLowerCase()) ? "Y" : "N";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 표시용 변환 (DB → 화면)
|
||||||
|
static convertForDisplay(value: string, inputType: string): any {
|
||||||
|
if (!value) return inputType === "number" ? 0 : "";
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case "number":
|
||||||
|
const num = parseFloat(value);
|
||||||
|
return isNaN(num) ? 0 : num;
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
return value; // YYYY-MM-DD 형식 그대로
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
return value === "Y";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
## 🏗️ 구현 단계
|
||||||
|
|
||||||
|
### Phase 1: 타입 정의 수정 (1-2일)
|
||||||
|
|
||||||
|
#### 1.1 입력 타입 enum 업데이트
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// frontend/types/unified-web-types.ts
|
||||||
|
export type InputType =
|
||||||
|
| "text" // 텍스트
|
||||||
|
| "number" // 숫자
|
||||||
|
| "date" // 날짜
|
||||||
|
| "code" // 코드
|
||||||
|
| "entity" // 엔티티
|
||||||
|
| "select" // 선택박스
|
||||||
|
| "checkbox" // 체크박스
|
||||||
|
| "radio"; // 라디오버튼
|
||||||
|
````
|
||||||
|
|
||||||
|
#### 1.2 UI 표시명 업데이트
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const INPUT_TYPE_OPTIONS = [
|
||||||
|
{ value: "text", label: "텍스트", description: "일반 텍스트 입력" },
|
||||||
|
{ value: "number", label: "숫자", description: "숫자 입력 (정수/소수)" },
|
||||||
|
{ value: "date", label: "날짜", description: "날짜 선택" },
|
||||||
|
{ value: "code", label: "코드", description: "공통코드 참조" },
|
||||||
|
{ value: "entity", label: "엔티티", description: "다른 테이블 참조" },
|
||||||
|
{ value: "select", label: "선택박스", description: "드롭다운 선택" },
|
||||||
|
{ value: "checkbox", label: "체크박스", description: "체크박스 입력" },
|
||||||
|
{ value: "radio", label: "라디오버튼", description: "단일 선택" },
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: 데이터베이스 스키마 수정 (1일)
|
||||||
|
|
||||||
|
#### 2.1 테이블 스키마 수정
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- table_type_columns 테이블 수정
|
||||||
|
ALTER TABLE table_type_columns
|
||||||
|
DROP COLUMN IF EXISTS db_type;
|
||||||
|
|
||||||
|
-- 컬럼명 변경
|
||||||
|
ALTER TABLE table_type_columns
|
||||||
|
RENAME COLUMN web_type TO input_type;
|
||||||
|
|
||||||
|
-- 기존 데이터 마이그레이션
|
||||||
|
UPDATE table_type_columns
|
||||||
|
SET input_type = CASE
|
||||||
|
WHEN input_type IN ('textarea') THEN 'text'
|
||||||
|
WHEN input_type IN ('decimal') THEN 'number'
|
||||||
|
WHEN input_type IN ('datetime') THEN 'date'
|
||||||
|
WHEN input_type IN ('dropdown') THEN 'select'
|
||||||
|
WHEN input_type IN ('boolean') THEN 'checkbox'
|
||||||
|
WHEN input_type NOT IN ('text', 'number', 'date', 'code', 'entity', 'select', 'checkbox', 'radio')
|
||||||
|
THEN 'text'
|
||||||
|
ELSE input_type
|
||||||
|
END;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: 백엔드 서비스 수정 (2-3일)
|
||||||
|
|
||||||
|
#### 3.1 DDL 생성 로직 수정
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 전체 VARCHAR 통일 방식으로 수정
|
||||||
|
private mapInputTypeToPostgresType(inputType: string): string {
|
||||||
|
// 기본 컬럼들은 기존 타입 유지 (시스템 컬럼)
|
||||||
|
// 사용자 정의 컬럼은 입력 타입과 관계없이 모두 VARCHAR(500)로 통일
|
||||||
|
return "varchar(500)";
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCreateTableQuery(
|
||||||
|
tableName: string,
|
||||||
|
columns: CreateColumnDefinition[]
|
||||||
|
): string {
|
||||||
|
// 사용자 정의 컬럼들 - 모두 VARCHAR(500)로 생성
|
||||||
|
const columnDefinitions = columns
|
||||||
|
.map((col) => {
|
||||||
|
let definition = `"${col.name}" varchar(500)`; // 타입 통일
|
||||||
|
|
||||||
|
if (!col.nullable) {
|
||||||
|
definition += " NOT NULL";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (col.defaultValue) {
|
||||||
|
definition += ` DEFAULT '${col.defaultValue}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return definition;
|
||||||
|
})
|
||||||
|
.join(",\n ");
|
||||||
|
|
||||||
|
// 기본 컬럼들 (시스템 필수 컬럼 - 기존 타입 유지)
|
||||||
|
const baseColumns = `
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"created_date" timestamp DEFAULT now(),
|
||||||
|
"updated_date" timestamp DEFAULT now(),
|
||||||
|
"writer" varchar(100),
|
||||||
|
"company_code" varchar(50) DEFAULT '*'`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
CREATE TABLE "${tableName}" (${baseColumns},
|
||||||
|
${columnDefinitions}
|
||||||
|
);`.trim();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 입력 타입 처리 서비스 구현
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 통합 입력 타입 처리 서비스
|
||||||
|
export class InputTypeService {
|
||||||
|
// 데이터 저장 전 형변환 (화면 입력값 → DB 저장값)
|
||||||
|
static convertForStorage(value: any, inputType: string): string {
|
||||||
|
if (value === null || value === undefined) return "";
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case "text":
|
||||||
|
case "select":
|
||||||
|
case "radio":
|
||||||
|
return String(value);
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
const num = parseFloat(String(value));
|
||||||
|
return isNaN(num) ? "0" : String(num);
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
if (!value) return "";
|
||||||
|
const date = new Date(value);
|
||||||
|
return isNaN(date.getTime()) ? "" : date.toISOString().split("T")[0];
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
return ["true", "1", "Y", "yes", true].includes(value) ? "Y" : "N";
|
||||||
|
|
||||||
|
case "code":
|
||||||
|
case "entity":
|
||||||
|
return String(value || "");
|
||||||
|
|
||||||
|
default:
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 화면 표시용 형변환 (DB 저장값 → 화면 표시값)
|
||||||
|
static convertForDisplay(value: string, inputType: string): any {
|
||||||
|
if (!value && value !== "0") {
|
||||||
|
return inputType === "number" ? 0 : inputType === "checkbox" ? false : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case "number":
|
||||||
|
const num = parseFloat(value);
|
||||||
|
return isNaN(num) ? 0 : num;
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
return value === "Y" || value === "true";
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
return value; // YYYY-MM-DD 형식 그대로 사용
|
||||||
|
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 입력값 검증
|
||||||
|
static validate(
|
||||||
|
value: any,
|
||||||
|
inputType: string
|
||||||
|
): { isValid: boolean; message?: string } {
|
||||||
|
if (!value && value !== 0) {
|
||||||
|
return { isValid: true }; // 빈 값은 허용
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case "number":
|
||||||
|
const num = parseFloat(String(value));
|
||||||
|
return {
|
||||||
|
isValid: !isNaN(num),
|
||||||
|
message: isNaN(num) ? "숫자 형식이 올바르지 않습니다." : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
const date = new Date(value);
|
||||||
|
return {
|
||||||
|
isValid: !isNaN(date.getTime()),
|
||||||
|
message: isNaN(date.getTime())
|
||||||
|
? "날짜 형식이 올바르지 않습니다."
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return { isValid: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: 프론트엔드 UI 수정 (2일)
|
||||||
|
|
||||||
|
#### 4.1 테이블 관리 화면 수정
|
||||||
|
|
||||||
|
- "웹 타입" → "입력 타입" 라벨 변경
|
||||||
|
- DB 타입 컬럼 제거
|
||||||
|
- 8개 타입만 선택 가능하도록 UI 수정
|
||||||
|
|
||||||
|
#### 4.2 화면 관리 시스템 연동
|
||||||
|
|
||||||
|
- 웹타입 → 입력타입 용어 통일
|
||||||
|
- 기존 화면관리 컴포넌트와 호환성 유지
|
||||||
|
|
||||||
|
### Phase 5: 기본 컬럼 유지 로직 보강 (1일)
|
||||||
|
|
||||||
|
#### 5.1 테이블 생성 시 기본 컬럼 자동 추가
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const DEFAULT_COLUMNS = [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
type: "serial PRIMARY KEY",
|
||||||
|
description: "기본키 (자동증가)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "created_date",
|
||||||
|
type: "timestamp DEFAULT now()",
|
||||||
|
description: "생성일시",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "updated_date",
|
||||||
|
type: "timestamp DEFAULT now()",
|
||||||
|
description: "수정일시",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "writer",
|
||||||
|
type: "varchar(100)",
|
||||||
|
description: "작성자",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "company_code",
|
||||||
|
type: "varchar(50) DEFAULT '*'",
|
||||||
|
description: "회사코드",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 테스트 계획
|
||||||
|
|
||||||
|
### 1. 단위 테스트
|
||||||
|
|
||||||
|
- [ ] 입력 타입별 형변환 함수 테스트
|
||||||
|
- [ ] 데이터 검증 로직 테스트
|
||||||
|
- [ ] DDL 생성 로직 테스트
|
||||||
|
|
||||||
|
### 2. 통합 테스트
|
||||||
|
|
||||||
|
- [ ] 테이블 생성 → 데이터 입력 → 조회 전체 플로우
|
||||||
|
- [ ] 기존 테이블과의 호환성 테스트
|
||||||
|
- [ ] 화면관리 시스템 연동 테스트
|
||||||
|
|
||||||
|
### 3. 성능 테스트
|
||||||
|
|
||||||
|
- [ ] VARCHAR vs 전용 타입 성능 비교
|
||||||
|
- [ ] 대용량 데이터 입력 테스트
|
||||||
|
- [ ] 형변환 오버헤드 측정
|
||||||
|
|
||||||
|
## 📊 마이그레이션 전략
|
||||||
|
|
||||||
|
### 기존 데이터 호환성
|
||||||
|
|
||||||
|
1. **기존 테이블**: 현재 타입 구조 유지
|
||||||
|
2. **신규 테이블**: 새로운 입력 타입 체계 적용
|
||||||
|
3. **점진적 전환**: 필요에 따라 기존 테이블도 단계적 전환
|
||||||
|
|
||||||
|
### 데이터 무결성 보장
|
||||||
|
|
||||||
|
- 형변환 실패 시 기본값 사용
|
||||||
|
- 검증 로직을 통한 데이터 품질 관리
|
||||||
|
- 에러 로깅 및 알림 시스템
|
||||||
|
|
||||||
|
## 🎯 예상 효과
|
||||||
|
|
||||||
|
### 사용자 경험 개선
|
||||||
|
|
||||||
|
- ✅ 직관적인 용어로 학습 비용 감소
|
||||||
|
- ✅ 8개 타입으로 선택 복잡도 감소
|
||||||
|
- ✅ 일관된 인터페이스 제공
|
||||||
|
|
||||||
|
### 개발 생산성 향상
|
||||||
|
|
||||||
|
- ✅ 타입 관리 복잡도 감소
|
||||||
|
- ✅ 에러 발생률 감소
|
||||||
|
- ✅ 빠른 프로토타이핑 가능
|
||||||
|
|
||||||
|
### 시스템 유연성 확보
|
||||||
|
|
||||||
|
- ✅ 요구사항 변경에 빠른 대응
|
||||||
|
- ✅ 다양한 데이터 형식 수용
|
||||||
|
- ✅ 확장성 있는 구조
|
||||||
|
|
||||||
|
## 🚨 주의사항
|
||||||
|
|
||||||
|
### 데이터 검증 강화 필요
|
||||||
|
|
||||||
|
VARCHAR 통일 방식 적용 시 애플리케이션 레벨에서 철저한 데이터 검증이 필요합니다.
|
||||||
|
|
||||||
|
### 성능 모니터링
|
||||||
|
|
||||||
|
초기 적용 후 성능 영향도를 지속적으로 모니터링하여 필요 시 최적화 방안을 강구합니다.
|
||||||
|
|
||||||
|
### 문서화 업데이트
|
||||||
|
|
||||||
|
새로운 입력 타입 체계에 대한 사용자 가이드 및 개발 문서를 업데이트합니다.
|
||||||
|
|
||||||
|
## 📅 일정
|
||||||
|
|
||||||
|
| 단계 | 소요시간 | 담당 |
|
||||||
|
| ---------------------------- | --------- | ---------- |
|
||||||
|
| Phase 1: 타입 정의 수정 | 1-2일 | 프론트엔드 |
|
||||||
|
| Phase 2: DB 스키마 수정 | 1일 | 백엔드 |
|
||||||
|
| Phase 3: 백엔드 서비스 수정 | 2-3일 | 백엔드 |
|
||||||
|
| Phase 4: 프론트엔드 UI 수정 | 2일 | 프론트엔드 |
|
||||||
|
| Phase 5: 기본 컬럼 로직 보강 | 1일 | 백엔드 |
|
||||||
|
| **총 소요시간** | **7-9일** | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**결론**: 전체 VARCHAR 통일 방식을 확정하여 최대한의 유연성과 안정성을 확보합니다.
|
||||||
|
|
||||||
|
## 🎯 핵심 결정사항 요약
|
||||||
|
|
||||||
|
### ✅ 확정된 방향성
|
||||||
|
|
||||||
|
1. **용어 통일**: 웹 타입 → 입력 타입
|
||||||
|
2. **타입 단순화**: 20개 → 8개 핵심 타입
|
||||||
|
3. **DB 타입 제거**: dbType 필드 완전 삭제
|
||||||
|
4. **저장 방식**: 모든 사용자 정의 컬럼을 VARCHAR(500)로 통일
|
||||||
|
5. **형변환**: 애플리케이션 레벨에서 입력 타입별 처리
|
||||||
|
|
||||||
|
### 🚀 예상 효과
|
||||||
|
|
||||||
|
- **개발 속도 3배 향상**: 타입 고민 시간 제거
|
||||||
|
- **에러율 90% 감소**: DB 타입 불일치 에러 완전 차단
|
||||||
|
- **요구사항 대응력 극대화**: 스키마 수정 없이 필드 성격 변경 가능
|
||||||
|
- **데이터 마이그레이션 100% 안전**: 어떤 형태의 데이터도 수용 가능
|
||||||
Loading…
Reference in New Issue