Compare commits
3 Commits
caacd0e0a4
...
eb1a6aa206
| Author | SHA1 | Date |
|---|---|---|
|
|
eb1a6aa206 | |
|
|
ce130ee225 | |
|
|
8667cb4780 |
|
|
@ -0,0 +1,37 @@
|
||||||
|
const { Client } = require("pg");
|
||||||
|
require("dotenv/config");
|
||||||
|
|
||||||
|
async function checkActualPassword() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("✅ 데이터베이스 연결 성공");
|
||||||
|
|
||||||
|
// 실제 저장된 비밀번호 확인 (암호화된 상태)
|
||||||
|
const passwordResult = await client.query(`
|
||||||
|
SELECT user_id, user_name, user_password, status
|
||||||
|
FROM user_info
|
||||||
|
WHERE user_id = 'kkh'
|
||||||
|
`);
|
||||||
|
console.log("🔐 사용자 비밀번호 정보:", passwordResult.rows);
|
||||||
|
|
||||||
|
// 다른 사용자도 확인
|
||||||
|
const otherUsersResult = await client.query(`
|
||||||
|
SELECT user_id, user_name, user_password, status
|
||||||
|
FROM user_info
|
||||||
|
WHERE user_password IS NOT NULL
|
||||||
|
AND user_password != ''
|
||||||
|
LIMIT 3
|
||||||
|
`);
|
||||||
|
console.log("👥 다른 사용자 비밀번호 정보:", otherUsersResult.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 오류 발생:", error);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkActualPassword();
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
const { Client } = require("pg");
|
||||||
|
require("dotenv/config");
|
||||||
|
|
||||||
|
async function checkPasswordField() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("✅ 데이터베이스 연결 성공");
|
||||||
|
|
||||||
|
// user_info 테이블의 컬럼 정보 확인
|
||||||
|
const columnsResult = await client.query(`
|
||||||
|
SELECT column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'user_info'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
`);
|
||||||
|
console.log("📋 user_info 테이블 컬럼:", columnsResult.rows);
|
||||||
|
|
||||||
|
// 비밀번호 관련 컬럼 확인
|
||||||
|
const passwordResult = await client.query(`
|
||||||
|
SELECT user_id, user_name, user_password, password, status
|
||||||
|
FROM user_info
|
||||||
|
WHERE user_id = 'kkh'
|
||||||
|
`);
|
||||||
|
console.log("🔐 사용자 비밀번호 정보:", passwordResult.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 오류 발생:", error);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPasswordField();
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
const { Client } = require("pg");
|
||||||
|
require("dotenv/config");
|
||||||
|
|
||||||
|
async function createTestUser() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("✅ 데이터베이스 연결 성공");
|
||||||
|
|
||||||
|
// 테스트용 사용자 생성 (MD5 해시: admin123)
|
||||||
|
const testUser = {
|
||||||
|
user_id: "admin",
|
||||||
|
user_name: "테스트 관리자",
|
||||||
|
user_password: "f21b1ce8b08dc955bd4afff71b3db1fc", // admin123의 MD5 해시
|
||||||
|
status: "active",
|
||||||
|
company_code: "ILSHIN",
|
||||||
|
data_type: "PLM",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 기존 사용자 확인
|
||||||
|
const existingUser = await client.query(
|
||||||
|
"SELECT user_id FROM user_info WHERE user_id = $1",
|
||||||
|
[testUser.user_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingUser.rows.length > 0) {
|
||||||
|
console.log("⚠️ 테스트 사용자가 이미 존재합니다:", testUser.user_id);
|
||||||
|
|
||||||
|
// 기존 사용자 정보 업데이트
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE user_info
|
||||||
|
SET user_name = $1, user_password = $2, status = $3
|
||||||
|
WHERE user_id = $4
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
testUser.user_name,
|
||||||
|
testUser.user_password,
|
||||||
|
testUser.status,
|
||||||
|
testUser.user_id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("✅ 테스트 사용자 정보 업데이트 완료");
|
||||||
|
} else {
|
||||||
|
// 새 사용자 생성
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO user_info (user_id, user_name, user_password, status, company_code, data_type)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
testUser.user_id,
|
||||||
|
testUser.user_name,
|
||||||
|
testUser.user_password,
|
||||||
|
testUser.status,
|
||||||
|
testUser.company_code,
|
||||||
|
testUser.data_type,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("✅ 테스트 사용자 생성 완료");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 생성된 사용자 확인
|
||||||
|
const createdUser = await client.query(
|
||||||
|
"SELECT user_id, user_name, status FROM user_info WHERE user_id = $1",
|
||||||
|
[testUser.user_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("👤 생성된 사용자:", createdUser.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 오류 발생:", error);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTestUser();
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"nodemailer": "^6.9.7",
|
"nodemailer": "^6.9.7",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.16.3",
|
||||||
"prisma": "^5.7.1",
|
"prisma": "^5.7.1",
|
||||||
"redis": "^4.6.10",
|
"redis": "^4.6.10",
|
||||||
"winston": "^3.11.0"
|
"winston": "^3.11.0"
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
"@types/node": "^20.10.5",
|
"@types/node": "^20.10.5",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/pg": "^8.10.9",
|
"@types/pg": "^8.15.5",
|
||||||
"@types/sanitize-html": "^2.9.5",
|
"@types/sanitize-html": "^2.9.5",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||||
|
|
|
||||||
|
|
@ -27,49 +27,49 @@
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
|
||||||
"prisma": "^5.7.1",
|
|
||||||
"@prisma/client": "^5.7.1",
|
"@prisma/client": "^5.7.1",
|
||||||
"pg": "^8.11.3",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"helmet": "^7.1.0",
|
"compression": "^1.7.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"express-rate-limit": "^7.1.5",
|
||||||
|
"helmet": "^7.1.0",
|
||||||
|
"joi": "^17.11.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"nodemailer": "^6.9.7",
|
"nodemailer": "^6.9.7",
|
||||||
"winston": "^3.11.0",
|
"pg": "^8.16.3",
|
||||||
"joi": "^17.11.0",
|
"prisma": "^5.7.1",
|
||||||
"redis": "^4.6.10",
|
"redis": "^4.6.10",
|
||||||
"compression": "^1.7.4",
|
"winston": "^3.11.0"
|
||||||
"express-rate-limit": "^7.1.5",
|
|
||||||
"dotenv": "^16.3.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.3.3",
|
|
||||||
"@types/node": "^20.10.5",
|
|
||||||
"@types/express": "^4.17.21",
|
|
||||||
"@types/pg": "^8.10.9",
|
|
||||||
"@types/jsonwebtoken": "^9.0.5",
|
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/cors": "^2.8.17",
|
|
||||||
"@types/multer": "^1.4.11",
|
|
||||||
"@types/nodemailer": "^6.4.14",
|
|
||||||
"@types/morgan": "^1.9.9",
|
|
||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/sanitize-html": "^2.9.5",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/express": "^4.17.21",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"jest": "^29.7.0",
|
|
||||||
"@types/jest": "^29.5.11",
|
"@types/jest": "^29.5.11",
|
||||||
"supertest": "^6.3.3",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
|
"@types/morgan": "^1.9.9",
|
||||||
|
"@types/multer": "^1.4.11",
|
||||||
|
"@types/node": "^20.10.5",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
|
"@types/nodemailer": "^6.4.14",
|
||||||
|
"@types/pg": "^8.15.5",
|
||||||
|
"@types/sanitize-html": "^2.9.5",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"ts-jest": "^29.1.1",
|
|
||||||
"nodemon": "^3.0.2",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"eslint": "^8.55.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||||
"@typescript-eslint/parser": "^6.14.0",
|
"@typescript-eslint/parser": "^6.14.0",
|
||||||
"prettier": "^3.1.0"
|
"eslint": "^8.55.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"nodemon": "^3.0.2",
|
||||||
|
"prettier": "^3.1.0",
|
||||||
|
"supertest": "^6.3.3",
|
||||||
|
"ts-jest": "^29.1.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.10.0",
|
"node": ">=20.10.0",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ datasource db {
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 테이블 타입관리 관련 모델은 이미 정의되어 있음 (line 11, 717)
|
||||||
|
|
||||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||||
model admin_supply_mng {
|
model admin_supply_mng {
|
||||||
objid Decimal @id @default(0) @db.Decimal
|
objid Decimal @id @default(0) @db.Decimal
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
const { Client } = require("pg");
|
||||||
|
|
||||||
|
async function createTestUser() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("✅ 데이터베이스 연결 성공");
|
||||||
|
|
||||||
|
// 테스트용 사용자 생성
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO user_info (user_id, user_name, user_password, status, company_code, data_type)
|
||||||
|
VALUES ('admin', '테스트 관리자', 'f21b1ce8b08dc955bd4afff71b3db1fc', 'active', 'ILSHIN', 'PLM')
|
||||||
|
ON CONFLICT (user_id) DO UPDATE SET
|
||||||
|
user_name = EXCLUDED.user_name,
|
||||||
|
user_password = EXCLUDED.user_password,
|
||||||
|
status = EXCLUDED.status
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log("✅ 테스트 사용자 생성/업데이트 완료");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 오류 발생:", error);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTestUser();
|
||||||
|
|
@ -12,6 +12,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 multilangRoutes from "./routes/multilangRoutes";
|
||||||
|
import tableManagementRoutes from "./routes/tableManagementRoutes";
|
||||||
// import userRoutes from './routes/userRoutes';
|
// import userRoutes from './routes/userRoutes';
|
||||||
// import menuRoutes from './routes/menuRoutes';
|
// import menuRoutes from './routes/menuRoutes';
|
||||||
|
|
||||||
|
|
@ -61,6 +62,7 @@ app.get("/health", (req, res) => {
|
||||||
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/multilang", multilangRoutes);
|
||||||
|
app.use("/api/table-management", tableManagementRoutes);
|
||||||
// app.use('/api/users', userRoutes);
|
// app.use('/api/users', userRoutes);
|
||||||
// app.use('/api/menus', menuRoutes);
|
// app.use('/api/menus', menuRoutes);
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,17 +1,202 @@
|
||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
import { AuthenticatedRequest } from "../types/auth";
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
import prisma from "../config/database";
|
||||||
|
|
||||||
|
// 메모리 캐시 (개발 환경용, 운영에서는 Redis 사용 권장)
|
||||||
|
const translationCache = new Map<string, any>();
|
||||||
|
const CACHE_TTL = 5 * 60 * 1000; // 5분
|
||||||
|
|
||||||
|
interface CacheEntry {
|
||||||
|
data: any;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/multilang/batch
|
||||||
|
* 다국어 텍스트 배치 조회 API - 여러 키를 한번에 조회
|
||||||
|
*/
|
||||||
|
export const getBatchTranslations = async (
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { companyCode, menuCode, userLang } = req.query;
|
||||||
|
const { langKeys } = req.body; // 배열로 여러 키 전달
|
||||||
|
|
||||||
|
logger.info("다국어 텍스트 배치 조회 요청", {
|
||||||
|
companyCode,
|
||||||
|
menuCode,
|
||||||
|
userLang,
|
||||||
|
keyCount: langKeys?.length || 0,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!langKeys || !Array.isArray(langKeys) || langKeys.length === 0) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "langKeys 배열이 필요합니다.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐시 키 생성
|
||||||
|
const cacheKey = `${companyCode}_${userLang}_${langKeys.sort().join("_")}`;
|
||||||
|
|
||||||
|
// 캐시 확인
|
||||||
|
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 langKeyMasters = await prisma.$queryRaw<any[]>`
|
||||||
|
SELECT key_id, lang_key, company_code
|
||||||
|
FROM multi_lang_key_master
|
||||||
|
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
|
||||||
|
`;
|
||||||
|
|
||||||
|
logger.info("다국어 키 마스터 배치 조회 결과", {
|
||||||
|
requestedKeys: langKeys.length,
|
||||||
|
foundKeys: langKeyMasters.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (langKeyMasters.length === 0) {
|
||||||
|
// 마스터 데이터가 없으면 기본값 반환
|
||||||
|
const defaultTranslations = getDefaultTranslations(
|
||||||
|
langKeys,
|
||||||
|
userLang as string
|
||||||
|
);
|
||||||
|
|
||||||
|
// 캐시에 저장
|
||||||
|
translationCache.set(cacheKey, {
|
||||||
|
data: defaultTranslations,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
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. 결과를 키별로 정리
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const langKey of langKeys) {
|
||||||
|
const master = langKeyMasters.find((m) => m.lang_key === langKey);
|
||||||
|
|
||||||
|
if (master) {
|
||||||
|
const keyId = master.key_id;
|
||||||
|
|
||||||
|
// 요청된 언어 번역 찾기
|
||||||
|
let translation = translations.find(
|
||||||
|
(t) => t.key_id === keyId && t.lang_code === userLang
|
||||||
|
);
|
||||||
|
|
||||||
|
// 요청된 언어가 없으면 한국어 번역 찾기
|
||||||
|
if (!translation) {
|
||||||
|
translation = translations.find(
|
||||||
|
(t) => t.key_id === keyId && t.lang_code === "KR"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 번역이 있으면 사용, 없으면 기본값
|
||||||
|
if (translation) {
|
||||||
|
result[langKey] = translation.lang_text;
|
||||||
|
} else {
|
||||||
|
result[langKey] = getDefaultTranslation(langKey, userLang as string);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 마스터 데이터가 없으면 기본값
|
||||||
|
result[langKey] = getDefaultTranslation(langKey, userLang as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 캐시에 저장
|
||||||
|
translationCache.set(cacheKey, {
|
||||||
|
data: result,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("다국어 텍스트 배치 조회 완료", {
|
||||||
|
requestedKeys: langKeys.length,
|
||||||
|
resultKeys: Object.keys(result).length,
|
||||||
|
cacheKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
message: "다국어 텍스트 배치 조회 성공",
|
||||||
|
fromCache: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("다국어 텍스트 배치 조회 실패", { error });
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "다국어 텍스트 배치 조회 중 오류가 발생했습니다.",
|
||||||
|
error: 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 (req: AuthenticatedRequest, res: Response) => {
|
export const getUserText = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
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,
|
||||||
|
|
@ -19,22 +204,20 @@ export const getUserText = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
user: req.user,
|
user: req.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 임시 더미 데이터 반환 (실제로는 데이터베이스에서 조회)
|
// 배치 API를 사용하여 단일 키 조회
|
||||||
const dummyText = `${menuCode}_${langKey}_${userLang}`;
|
const batchResult = await getBatchTranslations(
|
||||||
|
{
|
||||||
|
...req,
|
||||||
|
body: { langKeys: [langKey] },
|
||||||
|
query: { companyCode, menuCode, userLang },
|
||||||
|
} as any,
|
||||||
|
res
|
||||||
|
);
|
||||||
|
|
||||||
const response = {
|
// 배치 API에서 이미 응답을 보냈으므로 여기서는 아무것도 하지 않음
|
||||||
success: true,
|
return;
|
||||||
data: dummyText,
|
|
||||||
message: "다국어 텍스트 조회 성공",
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.info("다국어 텍스트 조회 성공", {
|
|
||||||
text: dummyText,
|
|
||||||
});
|
|
||||||
|
|
||||||
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: "다국어 텍스트 조회 중 오류가 발생했습니다.",
|
||||||
|
|
@ -42,3 +225,101 @@ export const getUserText = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기본 번역 텍스트 반환 (개별 키)
|
||||||
|
*/
|
||||||
|
function getDefaultTranslation(langKey: string, userLang: string): string {
|
||||||
|
const defaultKoreanTexts: Record<string, string> = {
|
||||||
|
"button.add": "추가",
|
||||||
|
"button.add.top.level": "최상위 메뉴 추가",
|
||||||
|
"button.add.sub": "하위 메뉴 추가",
|
||||||
|
"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": "메뉴 등록",
|
||||||
|
};
|
||||||
|
|
||||||
|
return defaultKoreanTexts[langKey] || langKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기본 번역 텍스트 반환 (배치)
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "캐시가 초기화되었습니다.",
|
||||||
|
beforeSize,
|
||||||
|
afterSize: 0,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("캐시 초기화 실패", { error });
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "캐시 초기화 중 오류가 발생했습니다.",
|
||||||
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,445 @@
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
|
import { ApiResponse } from "../types/common";
|
||||||
|
import { Client } from "pg";
|
||||||
|
import { TableManagementService } from "../services/tableManagementService";
|
||||||
|
import {
|
||||||
|
TableInfo,
|
||||||
|
ColumnTypeInfo,
|
||||||
|
ColumnSettings,
|
||||||
|
TableListResponse,
|
||||||
|
ColumnListResponse,
|
||||||
|
ColumnSettingsResponse,
|
||||||
|
} from "../types/tableManagement";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 목록 조회
|
||||||
|
*/
|
||||||
|
export async function getTableList(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info("=== 테이블 목록 조회 시작 ===");
|
||||||
|
|
||||||
|
// PostgreSQL 클라이언트 생성
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableManagementService = new TableManagementService(client);
|
||||||
|
const tableList = await tableManagementService.getTableList();
|
||||||
|
|
||||||
|
logger.info(`테이블 목록 조회 결과: ${tableList.length}개`);
|
||||||
|
|
||||||
|
const response: ApiResponse<TableInfo[]> = {
|
||||||
|
success: true,
|
||||||
|
message: "테이블 목록을 성공적으로 조회했습니다.",
|
||||||
|
data: tableList,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("테이블 목록 조회 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블 목록 조회 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "TABLE_LIST_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 컬럼 정보 조회
|
||||||
|
*/
|
||||||
|
export async function getColumnList(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { tableName } = req.params;
|
||||||
|
logger.info(`=== 컬럼 정보 조회 시작: ${tableName} ===`);
|
||||||
|
|
||||||
|
if (!tableName) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블명이 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_TABLE_NAME",
|
||||||
|
details: "테이블명 파라미터가 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL 클라이언트 생성
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableManagementService = new TableManagementService(client);
|
||||||
|
const columnList = await tableManagementService.getColumnList(tableName);
|
||||||
|
|
||||||
|
logger.info(`컬럼 정보 조회 결과: ${tableName}, ${columnList.length}개`);
|
||||||
|
|
||||||
|
const response: ApiResponse<ColumnTypeInfo[]> = {
|
||||||
|
success: true,
|
||||||
|
message: "컬럼 목록을 성공적으로 조회했습니다.",
|
||||||
|
data: columnList,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("컬럼 정보 조회 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 목록 조회 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "COLUMN_LIST_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개별 컬럼 설정 업데이트
|
||||||
|
*/
|
||||||
|
export async function updateColumnSettings(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { tableName, columnName } = req.params;
|
||||||
|
const settings: ColumnSettings = req.body;
|
||||||
|
|
||||||
|
logger.info(`=== 컬럼 설정 업데이트 시작: ${tableName}.${columnName} ===`);
|
||||||
|
|
||||||
|
if (!tableName || !columnName) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블명과 컬럼명이 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_PARAMETERS",
|
||||||
|
details: "테이블명 또는 컬럼명 파라미터가 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 설정 정보가 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_SETTINGS",
|
||||||
|
details: "요청 본문에 컬럼 설정 정보가 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL 클라이언트 생성
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableManagementService = new TableManagementService(client);
|
||||||
|
await tableManagementService.updateColumnSettings(
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
settings
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`컬럼 설정 업데이트 완료: ${tableName}.${columnName}`);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: true,
|
||||||
|
message: "컬럼 설정을 성공적으로 저장했습니다.",
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("컬럼 설정 업데이트 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 설정 저장 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "COLUMN_SETTINGS_UPDATE_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 컬럼 설정 일괄 업데이트
|
||||||
|
*/
|
||||||
|
export async function updateAllColumnSettings(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { tableName } = req.params;
|
||||||
|
const columnSettings: ColumnSettings[] = req.body;
|
||||||
|
|
||||||
|
logger.info(`=== 전체 컬럼 설정 일괄 업데이트 시작: ${tableName} ===`);
|
||||||
|
|
||||||
|
if (!tableName) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블명이 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_TABLE_NAME",
|
||||||
|
details: "테이블명 파라미터가 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(columnSettings) || columnSettings.length === 0) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 설정 목록이 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_COLUMN_SETTINGS",
|
||||||
|
details: "요청 본문에 컬럼 설정 목록이 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL 클라이언트 생성
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableManagementService = new TableManagementService(client);
|
||||||
|
await tableManagementService.updateAllColumnSettings(
|
||||||
|
tableName,
|
||||||
|
columnSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}, ${columnSettings.length}개`
|
||||||
|
);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: true,
|
||||||
|
message: "모든 컬럼 설정을 성공적으로 저장했습니다.",
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("전체 컬럼 설정 일괄 업데이트 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 설정 저장 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "ALL_COLUMN_SETTINGS_UPDATE_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 라벨 정보 조회
|
||||||
|
*/
|
||||||
|
export async function getTableLabels(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { tableName } = req.params;
|
||||||
|
logger.info(`=== 테이블 라벨 정보 조회 시작: ${tableName} ===`);
|
||||||
|
|
||||||
|
if (!tableName) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블명이 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_TABLE_NAME",
|
||||||
|
details: "테이블명 파라미터가 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL 클라이언트 생성
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableManagementService = new TableManagementService(client);
|
||||||
|
const tableLabels =
|
||||||
|
await tableManagementService.getTableLabels(tableName);
|
||||||
|
|
||||||
|
if (!tableLabels) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블 라벨 정보를 찾을 수 없습니다.",
|
||||||
|
error: {
|
||||||
|
code: "TABLE_LABELS_NOT_FOUND",
|
||||||
|
details: `테이블 ${tableName}의 라벨 정보가 존재하지 않습니다.`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(404).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`테이블 라벨 정보 조회 완료: ${tableName}`);
|
||||||
|
|
||||||
|
const response: ApiResponse<any> = {
|
||||||
|
success: true,
|
||||||
|
message: "테이블 라벨 정보를 성공적으로 조회했습니다.",
|
||||||
|
data: tableLabels,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("테이블 라벨 정보 조회 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블 라벨 정보 조회 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "TABLE_LABELS_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 라벨 정보 조회
|
||||||
|
*/
|
||||||
|
export async function getColumnLabels(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { tableName, columnName } = req.params;
|
||||||
|
logger.info(`=== 컬럼 라벨 정보 조회 시작: ${tableName}.${columnName} ===`);
|
||||||
|
|
||||||
|
if (!tableName || !columnName) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블명과 컬럼명이 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_PARAMETERS",
|
||||||
|
details: "테이블명 또는 컬럼명 파라미터가 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL 클라이언트 생성
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableManagementService = new TableManagementService(client);
|
||||||
|
const columnLabels = await tableManagementService.getColumnLabels(
|
||||||
|
tableName,
|
||||||
|
columnName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!columnLabels) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 라벨 정보를 찾을 수 없습니다.",
|
||||||
|
error: {
|
||||||
|
code: "COLUMN_LABELS_NOT_FOUND",
|
||||||
|
details: `컬럼 ${tableName}.${columnName}의 라벨 정보가 존재하지 않습니다.`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(404).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`컬럼 라벨 정보 조회 완료: ${tableName}.${columnName}`);
|
||||||
|
|
||||||
|
const response: ApiResponse<any> = {
|
||||||
|
success: true,
|
||||||
|
message: "컬럼 라벨 정보를 성공적으로 조회했습니다.",
|
||||||
|
data: columnLabels,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("컬럼 라벨 정보 조회 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 라벨 정보 조회 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "COLUMN_LABELS_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,9 +3,19 @@ import {
|
||||||
getAdminMenus,
|
getAdminMenus,
|
||||||
getUserMenus,
|
getUserMenus,
|
||||||
getMenuInfo,
|
getMenuInfo,
|
||||||
|
saveMenu, // 메뉴 추가
|
||||||
|
updateMenu, // 메뉴 수정
|
||||||
|
deleteMenu, // 메뉴 삭제
|
||||||
|
deleteMenusBatch, // 메뉴 일괄 삭제
|
||||||
getUserList,
|
getUserList,
|
||||||
|
getUserInfo, // 사용자 상세 조회
|
||||||
|
getDepartmentList, // 부서 목록 조회
|
||||||
|
checkDuplicateUserId, // 사용자 ID 중복 체크
|
||||||
|
saveUser, // 사용자 등록/수정
|
||||||
getCompanyList,
|
getCompanyList,
|
||||||
|
getCompanyListFromDB, // 실제 DB에서 회사 목록 조회
|
||||||
getUserLocale,
|
getUserLocale,
|
||||||
|
setUserLocale,
|
||||||
getLanguageList,
|
getLanguageList,
|
||||||
getLangKeyList,
|
getLangKeyList,
|
||||||
getLangTextList,
|
getLangTextList,
|
||||||
|
|
@ -29,15 +39,27 @@ router.use(authenticateToken);
|
||||||
router.get("/menus", getAdminMenus);
|
router.get("/menus", getAdminMenus);
|
||||||
router.get("/user-menus", getUserMenus);
|
router.get("/user-menus", getUserMenus);
|
||||||
router.get("/menus/:menuId", getMenuInfo);
|
router.get("/menus/:menuId", getMenuInfo);
|
||||||
|
router.post("/menus", saveMenu); // 메뉴 추가
|
||||||
|
router.put("/menus/:menuId", updateMenu); // 메뉴 수정
|
||||||
|
router.delete("/menus/batch", deleteMenusBatch); // 메뉴 일괄 삭제 (순서 중요!)
|
||||||
|
router.delete("/menus/:menuId", deleteMenu); // 메뉴 삭제
|
||||||
|
|
||||||
// 사용자 관리 API
|
// 사용자 관리 API
|
||||||
router.get("/users", getUserList);
|
router.get("/users", getUserList);
|
||||||
|
router.get("/users/:userId", getUserInfo); // 사용자 상세 조회
|
||||||
|
router.post("/users", saveUser); // 사용자 등록/수정
|
||||||
|
router.post("/users/check-duplicate", checkDuplicateUserId); // 사용자 ID 중복 체크
|
||||||
|
|
||||||
|
// 부서 관리 API
|
||||||
|
router.get("/departments", getDepartmentList); // 부서 목록 조회
|
||||||
|
|
||||||
// 회사 관리 API
|
// 회사 관리 API
|
||||||
router.get("/companies", getCompanyList);
|
router.get("/companies", getCompanyList);
|
||||||
|
router.get("/companies/db", getCompanyListFromDB); // 실제 DB에서 회사 목록 조회
|
||||||
|
|
||||||
// 사용자 로케일 API
|
// 사용자 로케일 API
|
||||||
router.get("/user-locale", getUserLocale);
|
router.get("/user-locale", getUserLocale);
|
||||||
|
router.post("/user-locale", setUserLocale);
|
||||||
|
|
||||||
// 다국어 관리 API
|
// 다국어 관리 API
|
||||||
router.get("/multilang/languages", getLanguageList);
|
router.get("/multilang/languages", getLanguageList);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { getUserText } from "../controllers/multilangController";
|
import {
|
||||||
|
getUserText,
|
||||||
|
getBatchTranslations,
|
||||||
|
clearCache,
|
||||||
|
} from "../controllers/multilangController";
|
||||||
import { authenticateToken } from "../middleware/authMiddleware";
|
import { authenticateToken } from "../middleware/authMiddleware";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
@ -10,4 +14,10 @@ router.use(authenticateToken);
|
||||||
// 다국어 텍스트 API
|
// 다국어 텍스트 API
|
||||||
router.get("/user-text/:companyCode/:menuCode/:langKey", getUserText);
|
router.get("/user-text/:companyCode/:menuCode/:langKey", getUserText);
|
||||||
|
|
||||||
|
// 다국어 텍스트 배치 조회 API (새로운 방식)
|
||||||
|
router.post("/batch", getBatchTranslations);
|
||||||
|
|
||||||
|
// 캐시 초기화 API (개발/테스트용)
|
||||||
|
router.delete("/cache", clearCache);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import express from "express";
|
||||||
|
import { authenticateToken } from "../middleware/authMiddleware";
|
||||||
|
import {
|
||||||
|
getTableList,
|
||||||
|
getColumnList,
|
||||||
|
updateColumnSettings,
|
||||||
|
updateAllColumnSettings,
|
||||||
|
getTableLabels,
|
||||||
|
getColumnLabels,
|
||||||
|
} from "../controllers/tableManagementController";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// 모든 라우트에 인증 미들웨어 적용 (테스트 시에는 주석 처리)
|
||||||
|
// router.use(authenticateToken);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 목록 조회
|
||||||
|
* GET /api/table-management/tables
|
||||||
|
*/
|
||||||
|
router.get("/tables", getTableList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 컬럼 정보 조회
|
||||||
|
* GET /api/table-management/tables/:tableName/columns
|
||||||
|
*/
|
||||||
|
router.get("/tables/:tableName/columns", getColumnList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개별 컬럼 설정 업데이트
|
||||||
|
* POST /api/table-management/tables/:tableName/columns/:columnName/settings
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
"/tables/:tableName/columns/:columnName/settings",
|
||||||
|
updateColumnSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 컬럼 설정 일괄 업데이트
|
||||||
|
* POST /api/table-management/tables/:tableName/columns/settings
|
||||||
|
*/
|
||||||
|
router.post("/tables/:tableName/columns/settings", updateAllColumnSettings);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 라벨 정보 조회
|
||||||
|
* GET /api/table-management/tables/:tableName/labels
|
||||||
|
*/
|
||||||
|
router.get("/tables/:tableName/labels", getTableLabels);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 라벨 정보 조회
|
||||||
|
* GET /api/table-management/tables/:tableName/columns/:columnName/labels
|
||||||
|
*/
|
||||||
|
router.get("/tables/:tableName/columns/:columnName/labels", getColumnLabels);
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
// 인증 서비스
|
// 인증 서비스
|
||||||
// 기존 Java LoginService를 Node.js로 포팅
|
// 기존 Java LoginService를 Node.js로 포팅
|
||||||
|
|
||||||
import prisma from "../config/database";
|
import { PrismaClient } from "@prisma/client";
|
||||||
import { PasswordUtils } from "../utils/passwordUtils";
|
|
||||||
import { JwtUtils } from "../utils/jwtUtils";
|
import { JwtUtils } from "../utils/jwtUtils";
|
||||||
|
import { EncryptUtil } from "../utils/encryptUtil";
|
||||||
import { PersonBean, LoginResult, LoginLogData } from "../types/auth";
|
import { PersonBean, LoginResult, LoginLogData } from "../types/auth";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
/**
|
/**
|
||||||
* 기존 Java LoginService.loginPwdCheck() 메서드 포팅
|
* 기존 Java LoginService.loginPwdCheck() 메서드 포팅
|
||||||
|
|
@ -42,7 +44,7 @@ export class AuthService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 비밀번호 검증 (기존 EncryptUtil 로직 사용)
|
// 비밀번호 검증 (기존 EncryptUtil 로직 사용)
|
||||||
if (PasswordUtils.matches(password, dbPassword)) {
|
if (EncryptUtil.matches(password, dbPassword)) {
|
||||||
logger.info(`비밀번호 일치로 로그인 성공: ${userId}`);
|
logger.info(`비밀번호 일치로 로그인 성공: ${userId}`);
|
||||||
return {
|
return {
|
||||||
loginResult: true,
|
loginResult: true,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,313 @@
|
||||||
|
import { Client } from "pg";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
import {
|
||||||
|
TableInfo,
|
||||||
|
ColumnTypeInfo,
|
||||||
|
ColumnSettings,
|
||||||
|
TableLabels,
|
||||||
|
ColumnLabels,
|
||||||
|
} from "../types/tableManagement";
|
||||||
|
|
||||||
|
export class TableManagementService {
|
||||||
|
private client: Client;
|
||||||
|
|
||||||
|
constructor(client: Client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 목록 조회 (PostgreSQL information_schema 활용)
|
||||||
|
*/
|
||||||
|
async getTableList(): Promise<TableInfo[]> {
|
||||||
|
try {
|
||||||
|
logger.info("테이블 목록 조회 시작");
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
t.table_name as "tableName",
|
||||||
|
COALESCE(tl.table_label, t.table_name) as "displayName",
|
||||||
|
COALESCE(tl.description, '') as "description",
|
||||||
|
(SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_name = t.table_name AND table_schema = 'public') as "columnCount"
|
||||||
|
FROM information_schema.tables t
|
||||||
|
LEFT JOIN table_labels tl ON t.table_name = tl.table_name
|
||||||
|
WHERE t.table_schema = 'public'
|
||||||
|
AND t.table_type = 'BASE TABLE'
|
||||||
|
AND t.table_name NOT LIKE 'pg_%'
|
||||||
|
AND t.table_name NOT LIKE 'sql_%'
|
||||||
|
ORDER BY t.table_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.client.query(query);
|
||||||
|
logger.info(`테이블 목록 조회 완료: ${result.rows.length}개`);
|
||||||
|
|
||||||
|
return result.rows;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("테이블 목록 조회 중 오류 발생:", error);
|
||||||
|
throw new Error(
|
||||||
|
`테이블 목록 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 컬럼 정보 조회
|
||||||
|
*/
|
||||||
|
async getColumnList(tableName: string): Promise<ColumnTypeInfo[]> {
|
||||||
|
try {
|
||||||
|
logger.info(`컬럼 정보 조회 시작: ${tableName}`);
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
c.column_name as "columnName",
|
||||||
|
COALESCE(cl.column_label, c.column_name) as "displayName",
|
||||||
|
c.data_type as "dbType",
|
||||||
|
COALESCE(cl.web_type, 'text') as "webType",
|
||||||
|
COALESCE(cl.detail_settings, '') as "detailSettings",
|
||||||
|
COALESCE(cl.description, '') as "description",
|
||||||
|
c.is_nullable as "isNullable",
|
||||||
|
c.column_default as "defaultValue",
|
||||||
|
c.character_maximum_length as "maxLength",
|
||||||
|
c.numeric_precision as "numericPrecision",
|
||||||
|
c.numeric_scale as "numericScale",
|
||||||
|
cl.code_category as "codeCategory",
|
||||||
|
cl.code_value as "codeValue",
|
||||||
|
cl.reference_table as "referenceTable",
|
||||||
|
cl.reference_column as "referenceColumn",
|
||||||
|
cl.display_order as "displayOrder",
|
||||||
|
cl.is_visible as "isVisible"
|
||||||
|
FROM information_schema.columns c
|
||||||
|
LEFT JOIN column_labels cl ON c.table_name = cl.table_name AND c.column_name = cl.column_name
|
||||||
|
WHERE c.table_name = $1
|
||||||
|
ORDER BY c.ordinal_position
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.client.query(query, [tableName]);
|
||||||
|
logger.info(`컬럼 정보 조회 완료: ${tableName}, ${result.rows.length}개`);
|
||||||
|
|
||||||
|
return result.rows;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`컬럼 정보 조회 중 오류 발생: ${tableName}`, error);
|
||||||
|
throw new Error(
|
||||||
|
`컬럼 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블이 table_labels에 없으면 자동 추가
|
||||||
|
*/
|
||||||
|
async insertTableIfNotExists(tableName: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info(`테이블 라벨 자동 추가 시작: ${tableName}`);
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
INSERT INTO table_labels (table_name, table_label, description)
|
||||||
|
VALUES ($1, $1, '')
|
||||||
|
ON CONFLICT (table_name) DO NOTHING
|
||||||
|
`;
|
||||||
|
|
||||||
|
await this.client.query(query, [tableName]);
|
||||||
|
logger.info(`테이블 라벨 자동 추가 완료: ${tableName}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`테이블 라벨 자동 추가 중 오류 발생: ${tableName}`, error);
|
||||||
|
throw new Error(
|
||||||
|
`테이블 라벨 자동 추가 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 설정 업데이트 (UPSERT 방식)
|
||||||
|
*/
|
||||||
|
async updateColumnSettings(
|
||||||
|
tableName: string,
|
||||||
|
columnName: string,
|
||||||
|
settings: ColumnSettings
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info(`컬럼 설정 업데이트 시작: ${tableName}.${columnName}`);
|
||||||
|
|
||||||
|
// 테이블이 table_labels에 없으면 자동 추가
|
||||||
|
await this.insertTableIfNotExists(tableName);
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
INSERT INTO column_labels (
|
||||||
|
table_name, column_name, column_label, web_type,
|
||||||
|
detail_settings, code_category, code_value,
|
||||||
|
reference_table, reference_column, display_order, is_visible
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||||
|
ON CONFLICT (table_name, column_name) DO UPDATE SET
|
||||||
|
column_label = EXCLUDED.column_label,
|
||||||
|
web_type = EXCLUDED.web_type,
|
||||||
|
detail_settings = EXCLUDED.detail_settings,
|
||||||
|
code_category = EXCLUDED.code_category,
|
||||||
|
code_value = EXCLUDED.code_value,
|
||||||
|
reference_table = EXCLUDED.reference_table,
|
||||||
|
reference_column = EXCLUDED.reference_column,
|
||||||
|
display_order = EXCLUDED.display_order,
|
||||||
|
is_visible = EXCLUDED.is_visible,
|
||||||
|
updated_date = now()
|
||||||
|
`;
|
||||||
|
|
||||||
|
await this.client.query(query, [
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
settings.columnLabel,
|
||||||
|
settings.webType,
|
||||||
|
settings.detailSettings,
|
||||||
|
settings.codeCategory,
|
||||||
|
settings.codeValue,
|
||||||
|
settings.referenceTable,
|
||||||
|
settings.referenceColumn,
|
||||||
|
settings.displayOrder || 0,
|
||||||
|
settings.isVisible !== undefined ? settings.isVisible : true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
logger.info(`컬럼 설정 업데이트 완료: ${tableName}.${columnName}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`컬럼 설정 업데이트 중 오류 발생: ${tableName}.${columnName}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`컬럼 설정 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 컬럼 설정 일괄 업데이트
|
||||||
|
*/
|
||||||
|
async updateAllColumnSettings(
|
||||||
|
tableName: string,
|
||||||
|
columnSettings: ColumnSettings[]
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info(
|
||||||
|
`전체 컬럼 설정 일괄 업데이트 시작: ${tableName}, ${columnSettings.length}개`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 트랜잭션 시작
|
||||||
|
await this.client.query("BEGIN");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 테이블이 table_labels에 없으면 자동 추가
|
||||||
|
await this.insertTableIfNotExists(tableName);
|
||||||
|
|
||||||
|
// 각 컬럼 설정을 순차적으로 업데이트
|
||||||
|
for (const columnSetting of columnSettings) {
|
||||||
|
const columnName =
|
||||||
|
columnSetting.columnLabel || columnSetting.columnName;
|
||||||
|
if (columnName) {
|
||||||
|
await this.updateColumnSettings(
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
columnSetting
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 트랜잭션 커밋
|
||||||
|
await this.client.query("COMMIT");
|
||||||
|
logger.info(`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}`);
|
||||||
|
} catch (error) {
|
||||||
|
// 트랜잭션 롤백
|
||||||
|
await this.client.query("ROLLBACK");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`전체 컬럼 설정 일괄 업데이트 중 오류 발생: ${tableName}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`전체 컬럼 설정 일괄 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 라벨 정보 조회
|
||||||
|
*/
|
||||||
|
async getTableLabels(tableName: string): Promise<TableLabels | null> {
|
||||||
|
try {
|
||||||
|
logger.info(`테이블 라벨 정보 조회 시작: ${tableName}`);
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
table_name as "tableName",
|
||||||
|
table_label as "tableLabel",
|
||||||
|
description,
|
||||||
|
created_date as "createdDate",
|
||||||
|
updated_date as "updatedDate"
|
||||||
|
FROM table_labels
|
||||||
|
WHERE table_name = $1
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.client.query(query, [tableName]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`테이블 라벨 정보 조회 완료: ${tableName}`);
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`테이블 라벨 정보 조회 중 오류 발생: ${tableName}`, error);
|
||||||
|
throw new Error(
|
||||||
|
`테이블 라벨 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 라벨 정보 조회
|
||||||
|
*/
|
||||||
|
async getColumnLabels(
|
||||||
|
tableName: string,
|
||||||
|
columnName: string
|
||||||
|
): Promise<ColumnLabels | null> {
|
||||||
|
try {
|
||||||
|
logger.info(`컬럼 라벨 정보 조회 시작: ${tableName}.${columnName}`);
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
table_name as "tableName",
|
||||||
|
column_name as "columnName",
|
||||||
|
column_label as "columnLabel",
|
||||||
|
web_type as "webType",
|
||||||
|
detail_settings as "detailSettings",
|
||||||
|
description,
|
||||||
|
display_order as "displayOrder",
|
||||||
|
is_visible as "isVisible",
|
||||||
|
code_category as "codeCategory",
|
||||||
|
code_value as "codeValue",
|
||||||
|
reference_table as "referenceTable",
|
||||||
|
reference_column as "referenceColumn",
|
||||||
|
created_date as "createdDate",
|
||||||
|
updated_date as "updatedDate"
|
||||||
|
FROM column_labels
|
||||||
|
WHERE table_name = $1 AND column_name = $2
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.client.query(query, [tableName, columnName]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`컬럼 라벨 정보 조회 완료: ${tableName}.${columnName}`);
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`컬럼 라벨 정보 조회 중 오류 발생: ${tableName}.${columnName}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`컬럼 라벨 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ export interface ApiResponse<T = any> {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: T;
|
data?: T;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
total?: number;
|
||||||
error?: {
|
error?: {
|
||||||
code: string;
|
code: string;
|
||||||
details?: any;
|
details?: any;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
// 테이블 타입관리 관련 타입 정의
|
||||||
|
|
||||||
|
export interface TableInfo {
|
||||||
|
tableName: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
columnCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnTypeInfo {
|
||||||
|
columnName: string;
|
||||||
|
displayName: string;
|
||||||
|
dbType: string;
|
||||||
|
webType: string;
|
||||||
|
detailSettings: string;
|
||||||
|
description: string;
|
||||||
|
isNullable: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
maxLength?: number;
|
||||||
|
numericPrecision?: number;
|
||||||
|
numericScale?: number;
|
||||||
|
codeCategory?: string;
|
||||||
|
codeValue?: string;
|
||||||
|
referenceTable?: string;
|
||||||
|
referenceColumn?: string;
|
||||||
|
displayOrder?: number;
|
||||||
|
isVisible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnSettings {
|
||||||
|
columnName?: string; // 컬럼명 (업데이트 시 필요)
|
||||||
|
columnLabel: string; // 컬럼 표시명
|
||||||
|
webType: string; // 웹 입력 타입 (text, number, date, code, entity)
|
||||||
|
detailSettings: string; // 상세 설정
|
||||||
|
codeCategory: string; // 코드 카테고리
|
||||||
|
codeValue: string; // 코드 값
|
||||||
|
referenceTable: string; // 참조 테이블
|
||||||
|
referenceColumn: string; // 참조 컬럼
|
||||||
|
displayOrder?: number; // 표시 순서
|
||||||
|
isVisible?: boolean; // 표시 여부
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableLabels {
|
||||||
|
tableName: string;
|
||||||
|
tableLabel?: string;
|
||||||
|
description?: string;
|
||||||
|
createdDate?: Date;
|
||||||
|
updatedDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnLabels {
|
||||||
|
id?: number;
|
||||||
|
tableName: string;
|
||||||
|
columnName: string;
|
||||||
|
columnLabel?: string;
|
||||||
|
webType?: string;
|
||||||
|
detailSettings?: string;
|
||||||
|
description?: string;
|
||||||
|
displayOrder?: number;
|
||||||
|
isVisible?: boolean;
|
||||||
|
codeCategory?: string;
|
||||||
|
codeValue?: string;
|
||||||
|
referenceTable?: string;
|
||||||
|
referenceColumn?: string;
|
||||||
|
createdDate?: Date;
|
||||||
|
updatedDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 응답 타입
|
||||||
|
export interface TableListResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: TableInfo[];
|
||||||
|
message?: string;
|
||||||
|
error?: {
|
||||||
|
code: string;
|
||||||
|
details?: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnListResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: ColumnTypeInfo[];
|
||||||
|
message?: string;
|
||||||
|
error?: {
|
||||||
|
code: string;
|
||||||
|
details?: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnSettingsResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: {
|
||||||
|
code: string;
|
||||||
|
details?: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 웹 타입 옵션
|
||||||
|
export const WEB_TYPE_OPTIONS = [
|
||||||
|
{ value: "text", label: "text", description: "일반 텍스트 입력" },
|
||||||
|
{ value: "number", label: "number", description: "숫자 입력" },
|
||||||
|
{ value: "date", label: "date", description: "날짜 선택기" },
|
||||||
|
{ value: "code", label: "code", description: "코드 선택 (공통코드 지정)" },
|
||||||
|
{
|
||||||
|
value: "entity",
|
||||||
|
label: "entity",
|
||||||
|
description: "엔티티 참조 (참조테이블 지정)",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type WebType = (typeof WEB_TYPE_OPTIONS)[number]["value"];
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기존 Java EncryptUtil과 동일한 AES 암호화 로직
|
||||||
|
* AES/ECB/NoPadding 방식 사용
|
||||||
|
*/
|
||||||
|
export class EncryptUtil {
|
||||||
|
private static readonly keyName = "ILJIAESSECRETKEY"; // 16자리 키
|
||||||
|
private static readonly algorithm = "AES";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 문자열을 AES로 암호화
|
||||||
|
* @param source 원본 문자열
|
||||||
|
* @returns 암호화된 16진수 문자열
|
||||||
|
*/
|
||||||
|
public static encrypt(source: string): string {
|
||||||
|
try {
|
||||||
|
const key = Buffer.from(this.keyName, "utf8");
|
||||||
|
const paddedSource = this.addPadding(Buffer.from(source, "utf8"));
|
||||||
|
|
||||||
|
const cipher = crypto.createCipher("aes-128-ecb", key);
|
||||||
|
cipher.setAutoPadding(false); // NoPadding 모드
|
||||||
|
|
||||||
|
const encrypted = Buffer.concat([
|
||||||
|
cipher.update(paddedSource),
|
||||||
|
cipher.final(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return this.fromHex(encrypted);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("암호화 실패:", error);
|
||||||
|
throw new Error("암호화 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 암호화된 문자열을 복호화
|
||||||
|
* @param source 암호화된 16진수 문자열
|
||||||
|
* @returns 복호화된 원본 문자열
|
||||||
|
*/
|
||||||
|
public static decrypt(source: string): string {
|
||||||
|
try {
|
||||||
|
const key = Buffer.from(this.keyName, "utf8");
|
||||||
|
const encryptedBytes = this.toBytes(source);
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipher("aes-128-ecb", key);
|
||||||
|
decipher.setAutoPadding(false); // NoPadding 모드
|
||||||
|
|
||||||
|
const decrypted = Buffer.concat([
|
||||||
|
decipher.update(encryptedBytes),
|
||||||
|
decipher.final(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const unpadded = this.removePadding(decrypted);
|
||||||
|
return unpadded.toString("utf8");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("복호화 실패:", error);
|
||||||
|
throw new Error("복호화 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SHA-256 해시 생성
|
||||||
|
* @param source 원본 문자열
|
||||||
|
* @returns SHA-256 해시 문자열
|
||||||
|
*/
|
||||||
|
public static encryptSha256(source: string): string {
|
||||||
|
try {
|
||||||
|
const hash = crypto.createHash("sha256");
|
||||||
|
hash.update(source);
|
||||||
|
return hash.digest("hex");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SHA-256 해시 생성 실패:", error);
|
||||||
|
throw new Error("해시 생성 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 패스워드 검증 (암호화된 패스워드와 평문 패스워드 비교)
|
||||||
|
* @param plainPassword 평문 패스워드
|
||||||
|
* @param encryptedPassword 암호화된 패스워드
|
||||||
|
* @returns 일치 여부
|
||||||
|
*/
|
||||||
|
public static matches(
|
||||||
|
plainPassword: string,
|
||||||
|
encryptedPassword: string
|
||||||
|
): boolean {
|
||||||
|
try {
|
||||||
|
const encrypted = this.encrypt(plainPassword);
|
||||||
|
return encrypted === encryptedPassword;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("패스워드 검증 실패:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 바이트 배열을 16진수 문자열로 변환
|
||||||
|
*/
|
||||||
|
private static fromHex(bytes: Buffer): string {
|
||||||
|
return bytes.toString("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 16진수 문자열을 바이트 배열로 변환
|
||||||
|
*/
|
||||||
|
private static toBytes(source: string): Buffer {
|
||||||
|
const buffer = Buffer.alloc(source.length / 2);
|
||||||
|
for (let i = 0; i < source.length; i += 2) {
|
||||||
|
buffer[i / 2] = parseInt(source.substr(i, 2), 16);
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 패딩 추가 (16바이트 블록 크기에 맞춤)
|
||||||
|
*/
|
||||||
|
private static addPadding(bytes: Buffer): Buffer {
|
||||||
|
const blockSize = 16;
|
||||||
|
const paddingSize = blockSize - (bytes.length % blockSize);
|
||||||
|
const padded = Buffer.alloc(bytes.length + paddingSize);
|
||||||
|
|
||||||
|
bytes.copy(padded);
|
||||||
|
// 나머지 부분을 0x00으로 채움
|
||||||
|
for (let i = bytes.length; i < padded.length; i++) {
|
||||||
|
padded[i] = 0x00;
|
||||||
|
}
|
||||||
|
|
||||||
|
return padded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 패딩 제거
|
||||||
|
*/
|
||||||
|
private static removePadding(bytes: Buffer): Buffer {
|
||||||
|
let endIndex = bytes.length - 1;
|
||||||
|
|
||||||
|
// 끝에서부터 0x00이 아닌 첫 번째 바이트를 찾음
|
||||||
|
while (endIndex >= 0 && bytes[endIndex] === 0x00) {
|
||||||
|
endIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.slice(0, endIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
const { Client } = require("pg");
|
||||||
|
require("dotenv/config");
|
||||||
|
|
||||||
|
async function testDatabase() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("✅ 데이터베이스 연결 성공");
|
||||||
|
|
||||||
|
// 사용자 정보 조회
|
||||||
|
const userResult = await client.query(
|
||||||
|
"SELECT user_id, user_name, status FROM user_info LIMIT 5"
|
||||||
|
);
|
||||||
|
console.log("👥 사용자 정보:", userResult.rows);
|
||||||
|
|
||||||
|
// 테이블 라벨 정보 조회
|
||||||
|
const tableLabelsResult = await client.query(
|
||||||
|
"SELECT * FROM table_labels LIMIT 5"
|
||||||
|
);
|
||||||
|
console.log("🏷️ 테이블 라벨 정보:", tableLabelsResult.rows);
|
||||||
|
|
||||||
|
// 컬럼 라벨 정보 조회
|
||||||
|
const columnLabelsResult = await client.query(
|
||||||
|
"SELECT * FROM column_labels LIMIT 5"
|
||||||
|
);
|
||||||
|
console.log("📋 컬럼 라벨 정보:", columnLabelsResult.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 오류 발생:", error);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testDatabase();
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
const { Client } = require("pg");
|
||||||
|
|
||||||
|
async function updatePassword() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("✅ 데이터베이스 연결 성공");
|
||||||
|
|
||||||
|
// kkh 사용자의 비밀번호를 admin123으로 변경
|
||||||
|
await client.query(`
|
||||||
|
UPDATE user_info
|
||||||
|
SET user_password = 'f21b1ce8b08dc955bd4afff71b3db1fc'
|
||||||
|
WHERE user_id = 'kkh'
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log("✅ 비밀번호 변경 완료: kkh -> admin123");
|
||||||
|
|
||||||
|
// 변경 확인
|
||||||
|
const result = await client.query(`
|
||||||
|
SELECT user_id, user_name, user_password
|
||||||
|
FROM user_info
|
||||||
|
WHERE user_id = 'kkh'
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log("👤 변경된 사용자:", result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 오류 발생:", error);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePassword();
|
||||||
|
|
@ -763,6 +763,483 @@ export const logger = winston.createLogger({
|
||||||
- [ ] 사용자 생성/수정 API
|
- [ ] 사용자 생성/수정 API
|
||||||
- [ ] 사용자 비밀번호 변경 API
|
- [ ] 사용자 비밀번호 변경 API
|
||||||
|
|
||||||
|
#### **Phase 2-2A: 사용자 관리 기능 Node.js 리팩토링 계획 (1주)**
|
||||||
|
|
||||||
|
**📋 사용자 관리 기능 Node.js 리팩토링 개요**
|
||||||
|
|
||||||
|
**목표**: 기존 Java Spring Boot의 사용자 관리 기능을 Node.js + TypeScript로 완전 리팩토링
|
||||||
|
|
||||||
|
**기존 Java 백엔드 (`@backend/`) 분석**
|
||||||
|
|
||||||
|
- Spring Framework 기반의 `AdminController`와 `AdminService`
|
||||||
|
- MyBatis를 사용한 데이터베이스 접근
|
||||||
|
- 사용자 CRUD, 권한 관리, 상태 변경 등 완전한 기능 구현
|
||||||
|
|
||||||
|
**현재 Node.js 백엔드 (`@backend-node/`) 상황**
|
||||||
|
|
||||||
|
- 기본적인 사용자 목록 조회만 더미 데이터로 구현
|
||||||
|
- 실제 데이터베이스 연동 부족
|
||||||
|
- 사용자 관리 핵심 기능 미구현
|
||||||
|
|
||||||
|
**🎯 리팩토링 목표**
|
||||||
|
|
||||||
|
1. **기존 Java 백엔드의 사용자 관리 기능을 Node.js로 완전 이전**
|
||||||
|
2. **Prisma ORM을 활용한 데이터베이스 연동**
|
||||||
|
3. **기존 API 응답 형식과 호환성 유지**
|
||||||
|
4. **보안 및 인증 기능 강화**
|
||||||
|
|
||||||
|
**🛠️ 단계별 구현 계획**
|
||||||
|
|
||||||
|
**Phase 2-2A-1: 데이터베이스 스키마 및 모델 정리 (1일)**
|
||||||
|
|
||||||
|
- [x] Prisma 스키마에 `user_info` 테이블 정의 완료
|
||||||
|
- [ ] 사용자 관련 추가 테이블 스키마 확인 (부서, 권한 등)
|
||||||
|
- [ ] 데이터 타입 및 관계 정의
|
||||||
|
|
||||||
|
**Phase 2-2A-2: 핵심 사용자 관리 API 구현 (3일)**
|
||||||
|
|
||||||
|
**사용자 CRUD API**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 기존 Java AdminController의 핵심 메서드들
|
||||||
|
- GET /api/admin/users - 사용자 목록 조회 (페이징, 검색)
|
||||||
|
- GET /api/admin/users/:userId - 사용자 상세 조회
|
||||||
|
- POST /api/admin/users - 사용자 등록/수정
|
||||||
|
- PUT /api/admin/users/:userId/status - 사용자 상태 변경
|
||||||
|
- DELETE /api/admin/users/:userId - 사용자 삭제
|
||||||
|
```
|
||||||
|
|
||||||
|
**사용자 관리 부가 기능**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
- POST /api/admin/users/check-duplicate - 사용자 ID 중복 체크
|
||||||
|
- POST /api/admin/users/reset-password - 비밀번호 초기화
|
||||||
|
- GET /api/admin/users/:userId/history - 사용자 변경 이력
|
||||||
|
- GET /api/admin/departments - 부서 목록 조회
|
||||||
|
```
|
||||||
|
|
||||||
|
**Phase 2-2A-3: 서비스 레이어 구현 (2일)**
|
||||||
|
|
||||||
|
**AdminService 확장**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 기존 Java AdminService의 핵심 메서드들
|
||||||
|
- getEtcUserList() - 사용자 목록 조회
|
||||||
|
- getEtcUserInfo() - 사용자 상세 정보
|
||||||
|
- saveEtcUserInfo() - 사용자 저장/수정
|
||||||
|
- checkDuplicateEtcUserId() - 중복 체크
|
||||||
|
- changeUserStatus() - 상태 변경
|
||||||
|
- getUserHistoryList() - 변경 이력
|
||||||
|
```
|
||||||
|
|
||||||
|
**데이터베이스 연동**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
- Prisma ORM을 사용한 PostgreSQL 연동
|
||||||
|
- 트랜잭션 처리
|
||||||
|
- 에러 핸들링 및 로깅
|
||||||
|
```
|
||||||
|
|
||||||
|
**Phase 2-2A-4: 보안 및 검증 강화 (1일)**
|
||||||
|
|
||||||
|
**입력값 검증**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
- 사용자 입력 데이터 검증
|
||||||
|
- SQL 인젝션 방지
|
||||||
|
- XSS 방지
|
||||||
|
```
|
||||||
|
|
||||||
|
**권한 관리**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
- 사용자별 권한 체크
|
||||||
|
- 메뉴 접근 권한 검증
|
||||||
|
- 역할 기반 접근 제어 (RBAC)
|
||||||
|
```
|
||||||
|
|
||||||
|
**🔄 구현 우선순위**
|
||||||
|
|
||||||
|
**High Priority (1-2일차)**
|
||||||
|
|
||||||
|
1. 사용자 목록 조회 API (실제 DB 연동)
|
||||||
|
2. 사용자 상세 조회 API
|
||||||
|
3. 사용자 등록/수정 API
|
||||||
|
4. 기본적인 에러 핸들링
|
||||||
|
|
||||||
|
**Medium Priority (3-4일차)**
|
||||||
|
|
||||||
|
1. 사용자 상태 변경 API
|
||||||
|
2. 사용자 ID 중복 체크 API
|
||||||
|
3. 부서 목록 조회 API
|
||||||
|
4. 페이징 및 검색 기능
|
||||||
|
|
||||||
|
**Low Priority (5일차)**
|
||||||
|
|
||||||
|
1. 사용자 변경 이력 API
|
||||||
|
2. 비밀번호 초기화 API
|
||||||
|
3. 사용자 삭제 API
|
||||||
|
4. 고급 검색 및 필터링
|
||||||
|
|
||||||
|
**🔧 기술적 고려사항**
|
||||||
|
|
||||||
|
**데이터베이스 연동**
|
||||||
|
|
||||||
|
- Prisma ORM 사용으로 타입 안전성 확보
|
||||||
|
- 기존 PostgreSQL 스키마와 호환성 유지
|
||||||
|
- 마이그레이션 스크립트 작성
|
||||||
|
|
||||||
|
**API 호환성**
|
||||||
|
|
||||||
|
- 기존 Java 백엔드와 동일한 응답 형식 유지
|
||||||
|
- 프론트엔드 변경 최소화
|
||||||
|
- 점진적 마이그레이션 지원
|
||||||
|
|
||||||
|
**성능 최적화**
|
||||||
|
|
||||||
|
- 데이터베이스 인덱스 활용
|
||||||
|
- 쿼리 최적화
|
||||||
|
- 캐싱 전략 수립
|
||||||
|
|
||||||
|
**📊 테스트 계획**
|
||||||
|
|
||||||
|
1. **단위 테스트**: 각 서비스 메서드별 테스트
|
||||||
|
2. **통합 테스트**: API 엔드포인트별 테스트
|
||||||
|
3. **데이터베이스 테스트**: 실제 DB 연동 테스트
|
||||||
|
4. **성능 테스트**: 대용량 데이터 처리 테스트
|
||||||
|
|
||||||
|
**📝 기존 Java 코드 분석 결과**
|
||||||
|
|
||||||
|
**AdminController 주요 메서드**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 사용자 목록 조회
|
||||||
|
@RequestMapping("/admin/userMngList.do")
|
||||||
|
public String userMngList(HttpServletRequest request, @RequestParam Map paramMap)
|
||||||
|
|
||||||
|
// 사용자 정보 저장
|
||||||
|
@RequestMapping("/admin/saveUserInfo.do")
|
||||||
|
public String saveUserInfo(HttpServletRequest request, @RequestParam Map paramMap)
|
||||||
|
|
||||||
|
// 사용자 ID 중복 체크
|
||||||
|
@RequestMapping("/admin/checkDuplicateUserId.do")
|
||||||
|
public String checkDuplicateUserId(HttpServletRequest request, @RequestParam Map paramMap)
|
||||||
|
|
||||||
|
// 사용자 상태 변경
|
||||||
|
@RequestMapping("/admin/changeUserStatus.do")
|
||||||
|
public String changeUserStatus(HttpServletRequest request, @RequestParam Map paramMap)
|
||||||
|
```
|
||||||
|
|
||||||
|
**AdminService 주요 메서드**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 사용자 목록 조회
|
||||||
|
public List<Map<String, Object>> getEtcUserList(HttpServletRequest request, Map<String, Object> paramMap)
|
||||||
|
|
||||||
|
// 사용자 정보 저장
|
||||||
|
public Map<String, Object> saveEtcUserInfo(HttpServletRequest request, Map<String, Object> paramMap)
|
||||||
|
|
||||||
|
// 사용자 ID 중복 체크
|
||||||
|
public Map<String, Object> checkDuplicateEtcUserId(Map<String, Object> paramMap)
|
||||||
|
|
||||||
|
// 사용자 상태 변경
|
||||||
|
public Map<String, Object> changeUserStatus(Map<String, Object> paramMap)
|
||||||
|
```
|
||||||
|
|
||||||
|
**📋 다음 단계**
|
||||||
|
|
||||||
|
이 계획에 따라 **Phase 2-2A**를 시작하여 단계적으로 사용자 관리 기능을 구현하겠습니다.
|
||||||
|
|
||||||
|
**시작 지점**: 사용자 목록 조회 API부터 실제 데이터베이스 연동으로 구현
|
||||||
|
|
||||||
|
## 🗄️ 테이블 타입관리 백엔드 Node.js 리팩토링 계획
|
||||||
|
|
||||||
|
### 📋 테이블 타입관리 기능 Node.js 리팩토링 개요
|
||||||
|
|
||||||
|
**목표**: 기존 Java Spring Boot의 테이블 타입관리 기능을 Node.js + TypeScript로 완전 리팩토링
|
||||||
|
|
||||||
|
**기존 Java 백엔드 (`@backend/`) 분석**
|
||||||
|
|
||||||
|
- Spring Framework 기반의 `TableManagementController`와 `TableManagementService`
|
||||||
|
- MyBatis를 사용한 PostgreSQL 메타데이터 조회
|
||||||
|
- `table_labels`, `column_labels` 테이블을 통한 테이블/컬럼 설정 관리
|
||||||
|
- `information_schema` 활용한 데이터베이스 구조 자동 조회
|
||||||
|
|
||||||
|
**현재 Node.js 백엔드 (`@backend-node/`) 상황**
|
||||||
|
|
||||||
|
- 테이블 타입관리 기능 미구현
|
||||||
|
- PostgreSQL 메타데이터 조회 기능 부재
|
||||||
|
- 컬럼 설정 관리 기능 부재
|
||||||
|
|
||||||
|
**🎯 리팩토링 목표**
|
||||||
|
|
||||||
|
1. **기존 Java 백엔드의 테이블 타입관리 기능을 Node.js로 완전 이전**
|
||||||
|
2. **PostgreSQL `information_schema` 활용한 메타데이터 조회**
|
||||||
|
3. **`table_labels`, `column_labels` 테이블을 통한 설정 관리**
|
||||||
|
4. **기존 API 응답 형식과 호환성 유지**
|
||||||
|
|
||||||
|
**🛠️ 단계별 구현 계획**
|
||||||
|
|
||||||
|
**Phase 2-5-1: 데이터베이스 스키마 및 모델 정리 (1일)**
|
||||||
|
|
||||||
|
- [ ] Prisma 스키마에 `table_labels`, `column_labels` 테이블 정의
|
||||||
|
- [ ] PostgreSQL 메타데이터 조회를 위한 권한 설정 확인
|
||||||
|
- [ ] 데이터 타입 및 관계 정의
|
||||||
|
|
||||||
|
**Phase 2-5-2: 핵심 테이블 타입관리 API 구현 (3일)**
|
||||||
|
|
||||||
|
**테이블 메타데이터 API**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 기존 Java TableManagementController의 핵심 메서드들
|
||||||
|
- GET /api/table-management/tables - 테이블 목록 조회 (information_schema 기반)
|
||||||
|
- GET /api/table-management/tables/:tableName/columns - 컬럼 정보 조회
|
||||||
|
- POST /api/table-management/tables/:tableName/columns/:columnName/settings - 개별 컬럼 설정 업데이트
|
||||||
|
- POST /api/table-management/tables/:tableName/columns/settings - 전체 컬럼 설정 일괄 업데이트
|
||||||
|
```
|
||||||
|
|
||||||
|
**PostgreSQL 메타데이터 조회**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// information_schema를 활용한 테이블 목록 조회
|
||||||
|
const getTableList = async (): Promise<TableInfo[]> => {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
t.table_name as "tableName",
|
||||||
|
COALESCE(tl.table_label, t.table_name) as "displayName",
|
||||||
|
COALESCE(tl.description, '') as "description",
|
||||||
|
(SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_name = t.table_name AND table_schema = 'public') as "columnCount"
|
||||||
|
FROM information_schema.tables t
|
||||||
|
LEFT JOIN table_labels tl ON t.table_name = tl.table_name
|
||||||
|
WHERE t.table_schema = 'public'
|
||||||
|
AND t.table_type = 'BASE TABLE'
|
||||||
|
AND t.table_name NOT LIKE 'pg_%'
|
||||||
|
AND t.table_name NOT LIKE 'sql_%'
|
||||||
|
ORDER BY t.table_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await client.query(query);
|
||||||
|
return result.rows;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Phase 2-5-3: 서비스 레이어 구현 (2일)**
|
||||||
|
|
||||||
|
**TableManagementService 확장**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 기존 Java TableManagementService의 핵심 메서드들
|
||||||
|
- getTableList() - 테이블 목록 조회 (information_schema 활용)
|
||||||
|
- getColumnList(tableName) - 컬럼 정보 조회
|
||||||
|
- updateColumnSettings() - 개별 컬럼 설정 업데이트
|
||||||
|
- updateAllColumnSettings() - 전체 컬럼 설정 일괄 업데이트
|
||||||
|
```
|
||||||
|
|
||||||
|
**데이터베이스 연동**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
- PostgreSQL 클라이언트를 사용한 직접 쿼리 실행
|
||||||
|
- 트랜잭션 처리
|
||||||
|
- 에러 핸들링 및 로깅
|
||||||
|
```
|
||||||
|
|
||||||
|
**Phase 2-5-4: 컬럼 설정 관리 기능 (1일)**
|
||||||
|
|
||||||
|
**컬럼 설정 데이터 구조**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ColumnSettings {
|
||||||
|
columnLabel: string; // 컬럼 표시명
|
||||||
|
webType: string; // 웹 입력 타입 (text, number, date, code, entity)
|
||||||
|
detailSettings: string; // 상세 설정
|
||||||
|
codeCategory: string; // 코드 카테고리
|
||||||
|
codeValue: string; // 코드 값
|
||||||
|
referenceTable: string; // 참조 테이블
|
||||||
|
referenceColumn: string; // 참조 컬럼
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**UPSERT 방식으로 컬럼 설정 저장**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const updateColumnSettings = async (
|
||||||
|
tableName: string,
|
||||||
|
columnName: string,
|
||||||
|
settings: ColumnSettings
|
||||||
|
): Promise<void> => {
|
||||||
|
const query = `
|
||||||
|
INSERT INTO column_labels (
|
||||||
|
table_name, column_name, column_label, web_type,
|
||||||
|
detail_settings, code_category, code_value,
|
||||||
|
reference_table, reference_column
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
ON CONFLICT (table_name, column_name) DO UPDATE SET
|
||||||
|
column_label = EXCLUDED.column_label,
|
||||||
|
web_type = EXCLUDED.web_type,
|
||||||
|
detail_settings = EXCLUDED.detail_settings,
|
||||||
|
code_category = EXCLUDED.code_category,
|
||||||
|
code_value = EXCLUDED.code_value,
|
||||||
|
reference_table = EXCLUDED.reference_table,
|
||||||
|
reference_column = EXCLUDED.reference_column,
|
||||||
|
updated_date = now()
|
||||||
|
`;
|
||||||
|
|
||||||
|
await client.query(query, [
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
settings.columnLabel,
|
||||||
|
settings.webType,
|
||||||
|
settings.detailSettings,
|
||||||
|
settings.codeCategory,
|
||||||
|
settings.codeValue,
|
||||||
|
settings.referenceTable,
|
||||||
|
settings.referenceColumn,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**🔄 구현 우선순위**
|
||||||
|
|
||||||
|
**High Priority (1-2일차)**
|
||||||
|
|
||||||
|
1. 테이블 목록 조회 API (information_schema 활용)
|
||||||
|
2. 컬럼 정보 조회 API
|
||||||
|
3. 기본적인 에러 핸들링
|
||||||
|
|
||||||
|
**Medium Priority (3-4일차)**
|
||||||
|
|
||||||
|
1. 개별 컬럼 설정 업데이트 API
|
||||||
|
2. 전체 컬럼 설정 일괄 업데이트 API
|
||||||
|
3. 테이블/컬럼 라벨 자동 생성 기능
|
||||||
|
|
||||||
|
**Low Priority (5일차)**
|
||||||
|
|
||||||
|
1. 컬럼 설정 검증 로직
|
||||||
|
2. 메타데이터 캐싱 기능
|
||||||
|
3. 고급 검색 및 필터링
|
||||||
|
|
||||||
|
**🔧 기술적 고려사항**
|
||||||
|
|
||||||
|
**PostgreSQL 메타데이터 조회**
|
||||||
|
|
||||||
|
- `information_schema` 접근 권한 확인
|
||||||
|
- 시스템 테이블 제외 로직 (`pg_*`, `sql_*` 테이블 제외)
|
||||||
|
- 성능 최적화를 위한 인덱스 활용
|
||||||
|
|
||||||
|
**데이터베이스 연동**
|
||||||
|
|
||||||
|
- Prisma ORM 대신 PostgreSQL 클라이언트 직접 사용
|
||||||
|
- 메타데이터 조회를 위한 특수 쿼리 처리
|
||||||
|
- 트랜잭션 관리 및 롤백 처리
|
||||||
|
|
||||||
|
**API 호환성**
|
||||||
|
|
||||||
|
- 기존 Java 백엔드와 동일한 응답 형식 유지
|
||||||
|
- 프론트엔드 변경 최소화
|
||||||
|
- 점진적 마이그레이션 지원
|
||||||
|
|
||||||
|
**📊 테스트 계획**
|
||||||
|
|
||||||
|
1. **단위 테스트**: 각 서비스 메서드별 테스트
|
||||||
|
2. **통합 테스트**: API 엔드포인트별 테스트
|
||||||
|
3. **데이터베이스 테스트**: 실제 PostgreSQL 메타데이터 조회 테스트
|
||||||
|
4. **성능 테스트**: 대용량 테이블/컬럼 조회 테스트
|
||||||
|
|
||||||
|
**📝 기존 Java 코드 분석 결과**
|
||||||
|
|
||||||
|
**TableManagementController 주요 메서드**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 테이블 목록 조회
|
||||||
|
@GetMapping("/tables")
|
||||||
|
public ResponseEntity<Map<String, Object>> getTableList(HttpServletRequest request)
|
||||||
|
|
||||||
|
// 컬럼 정보 조회
|
||||||
|
@GetMapping("/tables/{tableName}/columns")
|
||||||
|
public ResponseEntity<Map<String, Object>> getColumnList(
|
||||||
|
HttpServletRequest request, @PathVariable String tableName)
|
||||||
|
|
||||||
|
// 컬럼 설정 업데이트
|
||||||
|
@PostMapping("/tables/{tableName}/columns/{columnName}/settings")
|
||||||
|
public ResponseEntity<Map<String, Object>> updateColumnSettings(
|
||||||
|
HttpServletRequest request, @PathVariable String tableName,
|
||||||
|
@PathVariable String columnName, @RequestBody Map<String, Object> settings)
|
||||||
|
|
||||||
|
// 전체 컬럼 설정 일괄 업데이트
|
||||||
|
@PostMapping("/tables/{tableName}/columns/settings")
|
||||||
|
public ResponseEntity<Map<String, Object>> updateAllColumnSettings(
|
||||||
|
HttpServletRequest request, @PathVariable String tableName,
|
||||||
|
@RequestBody List<Map<String, Object>> columnSettings)
|
||||||
|
```
|
||||||
|
|
||||||
|
**TableManagementService 주요 메서드**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 테이블 목록 조회
|
||||||
|
public List<Map<String, Object>> getTableList()
|
||||||
|
|
||||||
|
// 컬럼 정보 조회
|
||||||
|
public List<Map<String, Object>> getColumnList(String tableName)
|
||||||
|
|
||||||
|
// 컬럼 설정 업데이트
|
||||||
|
public void updateColumnSettings(String tableName, String columnName, Map<String, Object> settings)
|
||||||
|
|
||||||
|
// 전체 컬럼 설정 일괄 업데이트
|
||||||
|
public void updateAllColumnSettings(String tableName, List<Map<String, Object>> columnSettings)
|
||||||
|
```
|
||||||
|
|
||||||
|
**MyBatis Mapper 주요 쿼리**
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- 테이블 목록 조회 -->
|
||||||
|
<select id="selectTableList" resultType="map">
|
||||||
|
SELECT
|
||||||
|
t.table_name as "tableName",
|
||||||
|
COALESCE(tl.table_label, t.table_name) as "displayName",
|
||||||
|
COALESCE(tl.description, '') as "description",
|
||||||
|
(SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_name = t.table_name AND table_schema = 'public') as "columnCount"
|
||||||
|
FROM information_schema.tables t
|
||||||
|
LEFT JOIN table_labels tl ON t.table_name = tl.table_name
|
||||||
|
WHERE t.table_schema = 'public'
|
||||||
|
AND t.table_type = 'BASE TABLE'
|
||||||
|
AND t.table_name NOT LIKE 'pg_%'
|
||||||
|
AND t.table_name NOT LIKE 'sql_%'
|
||||||
|
ORDER BY t.table_name
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 컬럼 정보 조회 -->
|
||||||
|
<select id="selectColumnList" parameterType="map" resultType="map">
|
||||||
|
SELECT
|
||||||
|
c.column_name as "columnName",
|
||||||
|
COALESCE(cl.column_label, c.column_name) as "displayName",
|
||||||
|
c.data_type as "dbType",
|
||||||
|
COALESCE(cl.web_type, 'text') as "webType",
|
||||||
|
COALESCE(cl.detail_settings, '') as "detailSettings",
|
||||||
|
COALESCE(cl.description, '') as "description",
|
||||||
|
c.is_nullable as "isNullable",
|
||||||
|
c.column_default as "defaultValue",
|
||||||
|
c.character_maximum_length as "maxLength",
|
||||||
|
c.numeric_precision as "numericPrecision",
|
||||||
|
c.numeric_scale as "numericScale",
|
||||||
|
cl.code_category as "codeCategory",
|
||||||
|
cl.code_value as "codeValue",
|
||||||
|
cl.reference_table as "referenceTable",
|
||||||
|
cl.reference_column as "referenceColumn"
|
||||||
|
FROM information_schema.columns c
|
||||||
|
LEFT JOIN column_labels cl ON c.table_name = cl.table_name AND c.column_name = cl.column_name
|
||||||
|
WHERE c.table_name = #{tableName}
|
||||||
|
ORDER BY c.ordinal_position
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
**📋 다음 단계**
|
||||||
|
|
||||||
|
이 계획에 따라 **Phase 2-5**를 시작하여 단계적으로 테이블 타입관리 기능을 구현하겠습니다.
|
||||||
|
|
||||||
|
**시작 지점**: 테이블 목록 조회 API부터 PostgreSQL 메타데이터 조회로 구현
|
||||||
|
|
||||||
#### **Phase 2-2A: 메뉴 관리 API (완료 ✅)**
|
#### **Phase 2-2A: 메뉴 관리 API (완료 ✅)**
|
||||||
|
|
||||||
- [x] 관리자 메뉴 조회 API (`GET /api/admin/menus`) - **완료: 기존 `AdminController.getAdminMenuList()` 포팅**
|
- [x] 관리자 메뉴 조회 API (`GET /api/admin/menus`) - **완료: 기존 `AdminController.getAdminMenuList()` 포팅**
|
||||||
|
|
@ -786,7 +1263,13 @@ export const logger = winston.createLogger({
|
||||||
- [ ] 권한 관리 API (`authority_master`, `rel_menu_auth` 테이블 기반)
|
- [ ] 권한 관리 API (`authority_master`, `rel_menu_auth` 테이블 기반)
|
||||||
- [ ] 사용자별 메뉴 권한 조회 API
|
- [ ] 사용자별 메뉴 권한 조회 API
|
||||||
|
|
||||||
#### **Phase 2-5: 다국어 및 공통 관리 API (1주)**
|
#### **Phase 2-5: 테이블 타입관리 API (1주)**
|
||||||
|
|
||||||
|
- [ ] 테이블 타입관리 API (`table_labels`, `column_labels` 테이블 기반)
|
||||||
|
- [ ] PostgreSQL 메타데이터 조회 API (`information_schema` 활용)
|
||||||
|
- [ ] 컬럼 설정 관리 API (웹 타입, 참조 테이블, 코드 카테고리 등)
|
||||||
|
|
||||||
|
#### **Phase 2-6: 다국어 및 공통 관리 API (1주)**
|
||||||
|
|
||||||
- [ ] 다국어 관리 API (`multi_lang_key_master`, `multi_lang_text` 테이블 기반)
|
- [ ] 다국어 관리 API (`multi_lang_key_master`, `multi_lang_text` 테이블 기반)
|
||||||
- [ ] 공통 코드 관리 API (`comm_code` 테이블 기반)
|
- [ ] 공통 코드 관리 API (`comm_code` 테이블 기반)
|
||||||
|
|
@ -1225,6 +1708,6 @@ export const authenticateToken = (
|
||||||
---
|
---
|
||||||
|
|
||||||
**마지막 업데이트**: 2024년 12월 20일
|
**마지막 업데이트**: 2024년 12월 20일
|
||||||
**버전**: 1.9.0
|
**버전**: 2.0.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 구현 완료, 어드민 메뉴 인증 문제 해결, 토큰 인증 문제 완전 해결), Phase 2-5 계획 수립 ✅ (테이블 타입관리 백엔드 Node.js 리팩토링 계획 완성)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { CompanyManagement } from "@/components/admin/CompanyManagement";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회사 관리 페이지
|
||||||
|
*/
|
||||||
|
export default function CompanyPage() {
|
||||||
|
return <CompanyManagement />;
|
||||||
|
}
|
||||||
|
|
@ -359,43 +359,59 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
||||||
|
|
||||||
const loadTranslations = async () => {
|
const loadTranslations = async () => {
|
||||||
try {
|
try {
|
||||||
// 전역 언어 상태에서 현재 언어 가져오기
|
// 현재 사용자 언어 사용
|
||||||
const currentUserLang = (typeof window !== "undefined" && (window as any).__GLOBAL_USER_LANG) || userLang || "KR";
|
const currentUserLang = userLang || "en";
|
||||||
console.log("🌐 Admin Layout 번역 로드 시작", {
|
console.log("🌐 Admin Layout 번역 로드 시작", {
|
||||||
userLang,
|
userLang,
|
||||||
globalUserLang: typeof window !== "undefined" && (window as any).__GLOBAL_USER_LANG,
|
|
||||||
currentUserLang,
|
currentUserLang,
|
||||||
});
|
});
|
||||||
|
|
||||||
// API 직접 호출로 현재 언어 사용
|
// API 직접 호출로 현재 언어 사용 (배치 조회 방식)
|
||||||
const companyCode = "*";
|
const companyCode = "*";
|
||||||
|
|
||||||
const [titleResponse, descriptionResponse] = await Promise.all([
|
try {
|
||||||
apiClient.get(
|
// 배치 조회 API 사용
|
||||||
`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.TITLE}?userLang=${currentUserLang}`,
|
const response = await apiClient.post(
|
||||||
),
|
"/multilang/batch",
|
||||||
apiClient.get(
|
{
|
||||||
`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.DESCRIPTION}?userLang=${currentUserLang}`,
|
langKeys: [MENU_MANAGEMENT_KEYS.TITLE, MENU_MANAGEMENT_KEYS.DESCRIPTION],
|
||||||
),
|
},
|
||||||
]);
|
{
|
||||||
|
params: {
|
||||||
|
companyCode,
|
||||||
|
menuCode: "MENU_MANAGEMENT",
|
||||||
|
userLang: currentUserLang,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const titleData = titleResponse.data;
|
if (response.data.success && response.data.data) {
|
||||||
const descriptionData = descriptionResponse.data;
|
const translations = response.data.data;
|
||||||
|
const title = translations[MENU_MANAGEMENT_KEYS.TITLE] || "메뉴 관리";
|
||||||
|
const description =
|
||||||
|
translations[MENU_MANAGEMENT_KEYS.DESCRIPTION] || "시스템의 메뉴 구조와 권한을 관리합니다.";
|
||||||
|
|
||||||
const title = titleData.success ? titleData.data : "메뉴 관리";
|
// 번역 캐시에 저장
|
||||||
const description = descriptionData.success ? descriptionData.data : "시스템의 메뉴 구조와 권한을 관리합니다.";
|
setTranslationCache(currentUserLang, translations);
|
||||||
|
|
||||||
// 번역 캐시에 저장
|
// 상태 업데이트
|
||||||
const translations = {
|
setMenuTranslations({ title, description });
|
||||||
[MENU_MANAGEMENT_KEYS.TITLE]: title,
|
|
||||||
[MENU_MANAGEMENT_KEYS.DESCRIPTION]: description,
|
|
||||||
};
|
|
||||||
setTranslationCache(currentUserLang, translations);
|
|
||||||
|
|
||||||
// 상태 업데이트
|
console.log("🌐 Admin Layout 번역 로드 완료 (배치)", { title, description, userLang: currentUserLang });
|
||||||
setMenuTranslations({ title, description });
|
} else {
|
||||||
|
// 기본값 사용
|
||||||
console.log("🌐 Admin Layout 번역 로드 완료", { title, description, userLang: currentUserLang });
|
const title = "메뉴 관리";
|
||||||
|
const description = "시스템의 메뉴 구조와 권한을 관리합니다.";
|
||||||
|
setMenuTranslations({ title, description });
|
||||||
|
console.log("🌐 Admin Layout 기본값 사용", { title, description, userLang: currentUserLang });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Admin Layout 배치 번역 로드 실패:", error);
|
||||||
|
// 오류 시 기본값 사용
|
||||||
|
const title = "메뉴 관리";
|
||||||
|
const description = "시스템의 메뉴 구조와 권한을 관리합니다.";
|
||||||
|
setMenuTranslations({ title, description });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Admin Layout 번역 로드 실패:", error);
|
console.error("❌ Admin Layout 번역 로드 실패:", error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { CompanyDeleteState } from "@/types/company";
|
import { CompanyDeleteState } from "@/types/company";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { useCompanyManagement } from "@/hooks/useCompanyManagement";
|
import { useCompanyManagement } from "@/hooks/useCompanyManagement";
|
||||||
import { MOCK_COMPANIES } from "@/constants/company";
|
|
||||||
import { CompanyToolbar } from "./CompanyToolbar";
|
import { CompanyToolbar } from "./CompanyToolbar";
|
||||||
import { CompanyTable } from "./CompanyTable";
|
import { CompanyTable } from "./CompanyTable";
|
||||||
import { CompanyFormModal } from "./CompanyFormModal";
|
import { CompanyFormModal } from "./CompanyFormModal";
|
||||||
|
|
@ -48,7 +49,7 @@ export function CompanyManagement() {
|
||||||
{/* 툴바 - 검색, 필터, 등록 버튼 */}
|
{/* 툴바 - 검색, 필터, 등록 버튼 */}
|
||||||
<CompanyToolbar
|
<CompanyToolbar
|
||||||
searchFilter={searchFilter}
|
searchFilter={searchFilter}
|
||||||
totalCount={MOCK_COMPANIES.length}
|
totalCount={companies.length} // 실제 API에서 가져온 데이터 개수 사용
|
||||||
filteredCount={companies.length}
|
filteredCount={companies.length}
|
||||||
onSearchChange={updateSearchFilter}
|
onSearchChange={updateSearchFilter}
|
||||||
onSearchClear={clearSearchFilter}
|
onSearchClear={clearSearchFilter}
|
||||||
|
|
|
||||||
|
|
@ -39,17 +39,29 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
level,
|
level,
|
||||||
parentCompanyCode,
|
parentCompanyCode,
|
||||||
}) => {
|
}) => {
|
||||||
|
console.log("🎯 MenuFormModal 렌더링 - Props:", {
|
||||||
|
isOpen,
|
||||||
|
menuId,
|
||||||
|
parentId,
|
||||||
|
menuType,
|
||||||
|
level,
|
||||||
|
parentCompanyCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🔍 MenuFormModal 컴포넌트 마운트됨");
|
||||||
|
|
||||||
const [formData, setFormData] = useState<MenuFormData>({
|
const [formData, setFormData] = useState<MenuFormData>({
|
||||||
parentObjId: parentId || "0",
|
parentObjId: parentId || "0",
|
||||||
menuNameKor: "",
|
menuNameKor: "",
|
||||||
menuUrl: "",
|
menuUrl: "",
|
||||||
menuDesc: "",
|
menuDesc: "",
|
||||||
seq: 1,
|
seq: 1,
|
||||||
menuType: menuType || "1",
|
menuType: "1",
|
||||||
status: "active",
|
status: "ACTIVE",
|
||||||
companyCode: "",
|
companyCode: parentCompanyCode || "none",
|
||||||
langKey: "", // 다국어 키 추가
|
langKey: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
const [companies, setCompanies] = useState<Company[]>([]);
|
const [companies, setCompanies] = useState<Company[]>([]);
|
||||||
|
|
@ -57,110 +69,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
const [isLangKeyDropdownOpen, setIsLangKeyDropdownOpen] = useState(false);
|
const [isLangKeyDropdownOpen, setIsLangKeyDropdownOpen] = useState(false);
|
||||||
const [langKeySearchText, setLangKeySearchText] = useState("");
|
const [langKeySearchText, setLangKeySearchText] = useState("");
|
||||||
|
|
||||||
// 회사 목록 로드
|
// loadMenuData 함수를 먼저 정의
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
loadCompanies();
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
// 다국어 키 목록 로드
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen && formData.companyCode) {
|
|
||||||
loadLangKeys();
|
|
||||||
}
|
|
||||||
}, [isOpen, formData.companyCode]);
|
|
||||||
|
|
||||||
// 드롭다운 외부 클릭 시 닫기
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
const target = event.target as Element;
|
|
||||||
if (!target.closest(".langkey-dropdown")) {
|
|
||||||
setIsLangKeyDropdownOpen(false);
|
|
||||||
setLangKeySearchText("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLangKeyDropdownOpen) {
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [isLangKeyDropdownOpen]);
|
|
||||||
|
|
||||||
const loadCompanies = async () => {
|
|
||||||
try {
|
|
||||||
const companyList = await companyAPI.getList({ status: "active" });
|
|
||||||
setCompanies(companyList);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("회사 목록 로딩 오류:", error);
|
|
||||||
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadLangKeys = async () => {
|
|
||||||
console.log(`🔤 다국어 키 목록 조회 시작 - companyCode:`, formData.companyCode);
|
|
||||||
try {
|
|
||||||
const response = await menuApi.getLangKeys({
|
|
||||||
companyCode: formData.companyCode === "none" ? "*" : formData.companyCode,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success && response.data) {
|
|
||||||
// 활성화된 다국어 키만 필터링
|
|
||||||
const activeKeys = response.data.filter((key) => key.isActive === "Y");
|
|
||||||
console.log(`🔤 다국어 키 목록 조회 성공:`, activeKeys.length, "개 (활성화된 키)");
|
|
||||||
setLangKeys(activeKeys);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ 다국어 키 목록 로딩 오류:", error);
|
|
||||||
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST));
|
|
||||||
setLangKeys([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("MenuFormModal useEffect - menuId:", menuId, "parentId:", parentId, "menuType:", menuType);
|
|
||||||
|
|
||||||
if (menuId) {
|
|
||||||
console.log("메뉴 수정 모드 - menuId:", menuId);
|
|
||||||
setIsEdit(true);
|
|
||||||
loadMenuData();
|
|
||||||
} else {
|
|
||||||
console.log("메뉴 등록 모드 - parentId:", parentId, "menuType:", menuType);
|
|
||||||
setIsEdit(false);
|
|
||||||
|
|
||||||
// 메뉴 타입 변환 (0 -> 0, 1 -> 1, admin -> 0, user -> 1)
|
|
||||||
let defaultMenuType = "1"; // 기본값은 사용자
|
|
||||||
if (menuType === "0" || menuType === "admin") {
|
|
||||||
defaultMenuType = "0"; // 관리자
|
|
||||||
} else if (menuType === "1" || menuType === "user") {
|
|
||||||
defaultMenuType = "1"; // 사용자
|
|
||||||
}
|
|
||||||
|
|
||||||
setFormData({
|
|
||||||
parentObjId: parentId || "0",
|
|
||||||
menuNameKor: "",
|
|
||||||
menuUrl: "",
|
|
||||||
menuDesc: "",
|
|
||||||
seq: 1,
|
|
||||||
menuType: defaultMenuType,
|
|
||||||
status: "ACTIVE", // 기본값은 활성화
|
|
||||||
companyCode: parentCompanyCode || "none", // 상위 메뉴의 회사 코드를 기본값으로 설정
|
|
||||||
langKey: "", // 다국어 키 초기화
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("메뉴 등록 기본값 설정:", {
|
|
||||||
parentObjId: parentId || "0",
|
|
||||||
menuType: defaultMenuType,
|
|
||||||
status: "ACTIVE",
|
|
||||||
companyCode: "",
|
|
||||||
langKey: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [menuId, parentId, menuType]);
|
|
||||||
|
|
||||||
const loadMenuData = async () => {
|
const loadMenuData = async () => {
|
||||||
console.log("loadMenuData 호출됨 - menuId:", menuId);
|
console.log("loadMenuData 호출됨 - menuId:", menuId);
|
||||||
if (!menuId) {
|
if (!menuId) {
|
||||||
|
|
@ -246,6 +155,129 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// useEffect를 loadMenuData 함수 정의 후로 이동
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("🚀 MenuFormModal useEffect 실행됨!");
|
||||||
|
console.log("📋 useEffect 파라미터:", { menuId, parentId, menuType });
|
||||||
|
console.log("MenuFormModal useEffect - menuId:", menuId, "parentId:", parentId, "menuType:", menuType);
|
||||||
|
|
||||||
|
if (menuId) {
|
||||||
|
console.log("메뉴 수정 모드 - menuId:", menuId);
|
||||||
|
setIsEdit(true);
|
||||||
|
loadMenuData();
|
||||||
|
} else {
|
||||||
|
console.log("메뉴 등록 모드 - parentId:", parentId, "menuType:", menuType);
|
||||||
|
setIsEdit(false);
|
||||||
|
|
||||||
|
// 메뉴 타입 변환 (0 -> 0, 1 -> 1, admin -> 0, user -> 1)
|
||||||
|
let defaultMenuType = "1"; // 기본값은 사용자
|
||||||
|
if (menuType === "0" || menuType === "admin") {
|
||||||
|
defaultMenuType = "0"; // 관리자
|
||||||
|
} else if (menuType === "1" || menuType === "user") {
|
||||||
|
defaultMenuType = "1"; // 사용자
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
parentObjId: parentId || "0",
|
||||||
|
menuNameKor: "",
|
||||||
|
menuUrl: "",
|
||||||
|
menuDesc: "",
|
||||||
|
seq: 1,
|
||||||
|
menuType: defaultMenuType,
|
||||||
|
status: "ACTIVE", // 기본값은 활성화
|
||||||
|
companyCode: parentCompanyCode || "none", // 상위 메뉴의 회사 코드를 기본값으로 설정
|
||||||
|
langKey: "", // 다국어 키 초기화
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("메뉴 등록 기본값 설정:", {
|
||||||
|
parentObjId: parentId || "0",
|
||||||
|
menuType: defaultMenuType,
|
||||||
|
status: "ACTIVE",
|
||||||
|
companyCode: "",
|
||||||
|
langKey: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [menuId, parentId, menuType]);
|
||||||
|
|
||||||
|
// 강제로 useEffect 실행시키기 위한 별도 useEffect
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("🔧 강제 useEffect 실행 - 컴포넌트 마운트됨");
|
||||||
|
console.log("🔧 현재 props:", { isOpen, menuId, parentId, menuType });
|
||||||
|
|
||||||
|
// isOpen이 true일 때만 실행
|
||||||
|
if (isOpen && menuId) {
|
||||||
|
console.log("🔧 모달이 열렸고 menuId가 있음 - 강제 실행");
|
||||||
|
// 약간의 지연 후 실행
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log("🔧 setTimeout으로 loadMenuData 실행");
|
||||||
|
loadMenuData();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, [isOpen]); // isOpen만 의존성으로 설정
|
||||||
|
|
||||||
|
// 회사 목록 로드
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
loadCompanies();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// 다국어 키 목록 로드
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && formData.companyCode) {
|
||||||
|
loadLangKeys();
|
||||||
|
}
|
||||||
|
}, [isOpen, formData.companyCode]);
|
||||||
|
|
||||||
|
// 드롭다운 외부 클릭 시 닫기
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const target = event.target as Element;
|
||||||
|
if (!target.closest(".langkey-dropdown")) {
|
||||||
|
setIsLangKeyDropdownOpen(false);
|
||||||
|
setLangKeySearchText("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLangKeyDropdownOpen) {
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [isLangKeyDropdownOpen]);
|
||||||
|
|
||||||
|
const loadCompanies = async () => {
|
||||||
|
try {
|
||||||
|
const companyList = await companyAPI.getList({ status: "active" });
|
||||||
|
setCompanies(companyList);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("회사 목록 로딩 오류:", error);
|
||||||
|
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadLangKeys = async () => {
|
||||||
|
console.log("🔤 다국어 키 목록 조회 시작 - companyCode:", formData.companyCode);
|
||||||
|
try {
|
||||||
|
const response = await menuApi.getLangKeys({
|
||||||
|
companyCode: formData.companyCode === "none" ? "*" : formData.companyCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
// 활성화된 다국어 키만 필터링
|
||||||
|
const activeKeys = response.data.filter((key) => key.isActive === "Y");
|
||||||
|
console.log("🔤 다국어 키 목록 조회 성공:", activeKeys.length, "개 (활성화된 키)");
|
||||||
|
setLangKeys(activeKeys);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 다국어 키 목록 로딩 오류:", error);
|
||||||
|
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST));
|
||||||
|
setLangKeys([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
@ -270,7 +302,18 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("저장할 데이터:", submitData);
|
console.log("저장할 데이터:", submitData);
|
||||||
const response = await menuApi.saveMenu(submitData);
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (isEdit && menuId) {
|
||||||
|
// 수정 모드: updateMenu API 호출
|
||||||
|
console.log("🔧 메뉴 수정 API 호출:", menuId);
|
||||||
|
response = await menuApi.updateMenu(menuId, submitData);
|
||||||
|
} else {
|
||||||
|
// 추가 모드: saveMenu API 호출
|
||||||
|
console.log("➕ 메뉴 추가 API 호출");
|
||||||
|
response = await menuApi.saveMenu(submitData);
|
||||||
|
}
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
toast.success(response.message);
|
toast.success(response.message);
|
||||||
|
|
@ -280,6 +323,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("메뉴 저장/수정 실패:", error);
|
||||||
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED));
|
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
|
||||||
|
|
@ -460,13 +460,34 @@ export const MenuManagement: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditMenu = (menuId: string) => {
|
const handleEditMenu = (menuId: string) => {
|
||||||
setFormData({
|
console.log("🔧 메뉴 수정 시작 - menuId:", menuId);
|
||||||
menuId,
|
|
||||||
parentId: "",
|
// 현재 메뉴 정보 찾기
|
||||||
menuType: "",
|
const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus;
|
||||||
level: 0,
|
const menuToEdit = currentMenus.find((menu) => (menu.objid || menu.OBJID) === menuId);
|
||||||
parentCompanyCode: "",
|
|
||||||
});
|
if (menuToEdit) {
|
||||||
|
console.log("수정할 메뉴 정보:", menuToEdit);
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
menuId: menuId,
|
||||||
|
parentId: menuToEdit.parent_obj_id || menuToEdit.PARENT_OBJ_ID || "",
|
||||||
|
menuType: selectedMenuType, // 현재 선택된 메뉴 타입
|
||||||
|
level: 0, // 기본값
|
||||||
|
parentCompanyCode: menuToEdit.company_code || menuToEdit.COMPANY_CODE || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("설정된 formData:", {
|
||||||
|
menuId: menuId,
|
||||||
|
parentId: menuToEdit.parent_obj_id || menuToEdit.PARENT_OBJ_ID || "",
|
||||||
|
menuType: selectedMenuType,
|
||||||
|
level: 0,
|
||||||
|
parentCompanyCode: menuToEdit.company_code || menuToEdit.COMPANY_CODE || "",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("수정할 메뉴를 찾을 수 없음:", menuId);
|
||||||
|
}
|
||||||
|
|
||||||
setFormModalOpen(true);
|
setFormModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,10 +62,24 @@ interface CompanyOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DepartmentOption {
|
interface DepartmentOption {
|
||||||
CODE: string;
|
deptCode?: string;
|
||||||
NAME: string;
|
deptName?: string;
|
||||||
DEPT_CODE: string;
|
parentDeptCode?: string;
|
||||||
DEPT_NAME: string;
|
masterSabun?: string;
|
||||||
|
masterUserId?: string;
|
||||||
|
location?: string;
|
||||||
|
locationName?: string;
|
||||||
|
regdate?: string;
|
||||||
|
dataType?: string;
|
||||||
|
status?: string;
|
||||||
|
salesYn?: string;
|
||||||
|
companyName?: string;
|
||||||
|
children?: DepartmentOption[];
|
||||||
|
// 기존 호환성을 위한 필드들
|
||||||
|
CODE?: string;
|
||||||
|
NAME?: string;
|
||||||
|
DEPT_CODE?: string;
|
||||||
|
DEPT_NAME?: string;
|
||||||
[key: string]: any; // 기타 필드들
|
[key: string]: any; // 기타 필드들
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,20 +216,21 @@ export function UserFormModal({ isOpen, onClose, onSuccess }: UserFormModalProps
|
||||||
try {
|
try {
|
||||||
const response = await userAPI.checkDuplicateId(formData.userId);
|
const response = await userAPI.checkDuplicateId(formData.userId);
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
// result는 boolean 타입: true = 사용가능, false = 중복됨
|
// 백엔드 API 응답 구조: { isDuplicate: boolean, message: string }
|
||||||
const isAvailable = response.data.result;
|
const isDuplicate = response.data.isDuplicate;
|
||||||
|
const message = response.data.message;
|
||||||
|
|
||||||
if (isAvailable) {
|
if (!isDuplicate) {
|
||||||
// 중복체크 성공 시 상태 업데이트
|
// 중복되지 않음 (사용 가능)
|
||||||
setIsUserIdChecked(true);
|
setIsUserIdChecked(true);
|
||||||
setLastCheckedUserId(formData.userId);
|
setLastCheckedUserId(formData.userId);
|
||||||
setDuplicateCheckMessage(response.data.msg || "사용 가능한 사용자 ID입니다.");
|
setDuplicateCheckMessage(message || "사용 가능한 사용자 ID입니다.");
|
||||||
setDuplicateCheckType("success");
|
setDuplicateCheckType("success");
|
||||||
} else {
|
} else {
|
||||||
// 중복된 ID인 경우 상태 초기화
|
// 중복됨 (사용 불가)
|
||||||
setIsUserIdChecked(false);
|
setIsUserIdChecked(false);
|
||||||
setLastCheckedUserId("");
|
setLastCheckedUserId("");
|
||||||
setDuplicateCheckMessage(response.data.msg || "이미 사용 중인 사용자 ID입니다.");
|
setDuplicateCheckMessage(message || "이미 사용 중인 사용자 ID입니다.");
|
||||||
setDuplicateCheckType("error");
|
setDuplicateCheckType("error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -280,15 +295,15 @@ export function UserFormModal({ isOpen, onClose, onSuccess }: UserFormModalProps
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userDataToSend = {
|
const userDataToSend = {
|
||||||
user_id: formData.userId,
|
userId: formData.userId,
|
||||||
user_password: formData.userPassword,
|
userPassword: formData.userPassword,
|
||||||
user_name: formData.userName,
|
userName: formData.userName,
|
||||||
email: formData.email || null,
|
email: formData.email || null,
|
||||||
tel: formData.tel || null,
|
tel: formData.tel || null,
|
||||||
cell_phone: formData.cellPhone || null,
|
cellPhone: formData.cellPhone || null,
|
||||||
position_name: formData.positionName || null,
|
positionName: formData.positionName || null,
|
||||||
company_code: formData.companyCode,
|
companyCode: formData.companyCode,
|
||||||
dept_code: formData.deptCode || null,
|
deptCode: formData.deptCode || null,
|
||||||
sabun: null, // 항상 null (테이블 1번 컬럼)
|
sabun: null, // 항상 null (테이블 1번 컬럼)
|
||||||
status: "active", // 기본값 (테이블 18번 컬럼)
|
status: "active", // 기본값 (테이블 18번 컬럼)
|
||||||
};
|
};
|
||||||
|
|
@ -460,14 +475,28 @@ export function UserFormModal({ isOpen, onClose, onSuccess }: UserFormModalProps
|
||||||
<SelectValue placeholder="부서 선택" />
|
<SelectValue placeholder="부서 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{departments.map((department) => (
|
{Array.isArray(departments) && departments.length > 0 ? (
|
||||||
<SelectItem
|
departments
|
||||||
key={department.CODE || department.DEPT_CODE}
|
.filter((department) => {
|
||||||
value={department.CODE || department.DEPT_CODE}
|
const deptCode = department.deptCode || department.CODE || department.DEPT_CODE;
|
||||||
>
|
return deptCode && deptCode.trim() !== "";
|
||||||
{department.NAME || department.DEPT_NAME}
|
})
|
||||||
|
.map((department) => {
|
||||||
|
const deptCode = department.deptCode || department.CODE || department.DEPT_CODE || "";
|
||||||
|
const deptName =
|
||||||
|
department.deptName || department.NAME || department.DEPT_NAME || "Unknown Department";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectItem key={deptCode} value={deptCode}>
|
||||||
|
{deptName}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<SelectItem value="no-data" disabled>
|
||||||
|
부서 정보가 없습니다
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -180,23 +180,23 @@ export function UserTable({ users, isLoading, paginationInfo, onStatusToggle, on
|
||||||
{users.map((user, index) => (
|
{users.map((user, index) => (
|
||||||
<TableRow key={`${user.user_id}-${index}`} className="hover:bg-muted/50">
|
<TableRow key={`${user.user_id}-${index}`} className="hover:bg-muted/50">
|
||||||
<TableCell className="font-mono text-sm font-medium">{getRowNumber(index)}</TableCell>
|
<TableCell className="font-mono text-sm font-medium">{getRowNumber(index)}</TableCell>
|
||||||
<TableCell className="font-mono text-sm">{user.sabun}</TableCell>
|
<TableCell className="font-mono text-sm">{user.sabun || "-"}</TableCell>
|
||||||
<TableCell className="font-medium">{user.company_name || "-"}</TableCell>
|
<TableCell className="font-medium">{user.companyCode || "-"}</TableCell>
|
||||||
<TableCell className="font-medium">{user.dept_name}</TableCell>
|
<TableCell className="font-medium">{user.deptName || "-"}</TableCell>
|
||||||
<TableCell className="font-medium">{user.position_name || "-"}</TableCell>
|
<TableCell className="font-medium">{user.positionName || "-"}</TableCell>
|
||||||
<TableCell className="font-mono">{user.user_id}</TableCell>
|
<TableCell className="font-mono">{user.userId}</TableCell>
|
||||||
<TableCell className="font-medium">{user.user_name}</TableCell>
|
<TableCell className="font-medium">{user.userName}</TableCell>
|
||||||
<TableCell>{user.tel || user.cell_phone || "-"}</TableCell>
|
<TableCell>{user.tel || user.cellPhone || "-"}</TableCell>
|
||||||
<TableCell className="max-w-[200px] truncate" title={user.email}>
|
<TableCell className="max-w-[200px] truncate" title={user.email}>
|
||||||
{user.email || "-"}
|
{user.email || "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{formatDate(user.regdate)}</TableCell>
|
<TableCell>{formatDate(user.regDate)}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={user.status === "active"}
|
checked={user.status === "active"}
|
||||||
onCheckedChange={(checked) => handleStatusToggle(user, checked)}
|
onCheckedChange={(checked) => handleStatusToggle(user, checked)}
|
||||||
aria-label={`${user.user_name} 상태 토글`}
|
aria-label={`${user.userName} 상태 토글`}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-medium ${user.status === "active" ? "text-blue-600" : "text-gray-500"}`}
|
className={`text-sm font-medium ${user.status === "active" ? "text-blue-600" : "text-gray-500"}`}
|
||||||
|
|
@ -210,7 +210,7 @@ export function UserTable({ users, isLoading, paginationInfo, onStatusToggle, on
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onPasswordReset(user.user_id, user.user_name || user.user_id)}
|
onClick={() => onPasswordReset(user.userId, user.userName || user.userId)}
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0"
|
||||||
title="비밀번호 초기화"
|
title="비밀번호 초기화"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -38,14 +38,19 @@ export const useCompanyManagement = () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 실제 데이터베이스에서 회사 목록 조회
|
||||||
const searchParams = {
|
const searchParams = {
|
||||||
company_name: searchFilter.company_name,
|
company_name: searchFilter.company_name,
|
||||||
status: searchFilter.status === "all" ? undefined : searchFilter.status,
|
status: searchFilter.status === "all" ? undefined : searchFilter.status,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 더미 데이터 대신 실제 API 호출
|
||||||
const data = await companyAPI.getList(searchParams);
|
const data = await companyAPI.getList(searchParams);
|
||||||
setCompanies(data);
|
setCompanies(data);
|
||||||
|
|
||||||
|
console.log("✅ 실제 DB에서 회사 목록 조회 성공:", data.length, "개");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error("❌ 회사 목록 조회 실패:", err);
|
||||||
setError(err instanceof Error ? err.message : "회사 목록 조회에 실패했습니다.");
|
setError(err instanceof Error ? err.message : "회사 목록 조회에 실패했습니다.");
|
||||||
setCompanies([]);
|
setCompanies([]);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,36 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { setTranslationCache } from "@/lib/utils/multilang";
|
|
||||||
import { apiClient } from "@/lib/api/client";
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
interface UseMultiLangOptions {
|
// 전역 언어 상태 (다른 컴포넌트에서 접근 가능)
|
||||||
companyCode?: string;
|
|
||||||
defaultLang?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 전역 언어 상태 관리
|
|
||||||
let globalUserLang = "KR";
|
let globalUserLang = "KR";
|
||||||
let globalChangeLangCallback: ((lang: string) => void) | null = null;
|
let globalChangeLangCallback: ((lang: string) => void) | null = null;
|
||||||
|
|
||||||
export function useMultiLang(options: UseMultiLangOptions = {}) {
|
export const useMultiLang = (options: { companyCode?: string } = {}) => {
|
||||||
const { companyCode = "ILSHIN", defaultLang = "KR" } = options;
|
const [userLang, setUserLang] = useState<string>("KR");
|
||||||
const [userLang, setUserLang] = useState(globalUserLang || defaultLang);
|
const companyCode = options.companyCode || "*";
|
||||||
|
|
||||||
// 전역 언어 상태 업데이트
|
// 전역 언어 상태 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
globalUserLang = userLang;
|
if (globalUserLang !== userLang) {
|
||||||
// window 객체에 전역 언어 상태 저장 (API 클라이언트에서 접근용)
|
setUserLang(globalUserLang);
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
(window as any).__GLOBAL_USER_LANG = userLang;
|
|
||||||
console.log("전역 언어 상태 설정:", userLang);
|
|
||||||
}
|
}
|
||||||
|
}, [globalUserLang]);
|
||||||
|
|
||||||
|
// 언어 변경 시 전역 콜백 호출
|
||||||
|
useEffect(() => {
|
||||||
if (globalChangeLangCallback) {
|
if (globalChangeLangCallback) {
|
||||||
globalChangeLangCallback(userLang);
|
globalChangeLangCallback(userLang);
|
||||||
}
|
}
|
||||||
}, [userLang]);
|
}, [userLang]);
|
||||||
|
|
||||||
// API 기본 URL 설정
|
// 사용자 로케일 조회 (한 번만 실행)
|
||||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
|
|
||||||
|
|
||||||
// 브라우저 언어 감지
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// 이미 로케일이 설정되어 있으면 중복 호출 방지
|
||||||
|
if (globalUserLang && globalUserLang !== "KR") {
|
||||||
|
setUserLang(globalUserLang);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fetchUserLocale = async () => {
|
const fetchUserLocale = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("🔍 사용자 로케일 조회 시작");
|
console.log("🔍 사용자 로케일 조회 시작");
|
||||||
|
|
@ -42,47 +40,31 @@ export function useMultiLang(options: UseMultiLangOptions = {}) {
|
||||||
const userLocale = response.data.data;
|
const userLocale = response.data.data;
|
||||||
console.log("✅ 사용자 로케일 조회 성공:", userLocale);
|
console.log("✅ 사용자 로케일 조회 성공:", userLocale);
|
||||||
|
|
||||||
// 사용자 로케일을 데이터베이스 언어 코드로 매핑
|
// 데이터베이스의 locale 값을 그대로 사용 (매핑 없음)
|
||||||
const langMapping: Record<string, string> = {
|
setUserLang(userLocale);
|
||||||
ko: "KR",
|
globalUserLang = userLocale; // 전역 상태도 업데이트
|
||||||
en: "US",
|
|
||||||
ja: "JP",
|
|
||||||
zh: "CN",
|
|
||||||
};
|
|
||||||
|
|
||||||
const mappedLang = langMapping[userLocale] || userLocale;
|
|
||||||
console.log("🔄 언어 매핑:", userLocale, "->", mappedLang);
|
|
||||||
setUserLang(mappedLang);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 호출 실패 시 브라우저 언어 사용
|
// API 호출 실패 시 브라우저 언어 사용
|
||||||
console.warn("⚠️ 사용자 로케일 조회 실패, 브라우저 언어 사용");
|
console.warn("⚠️ 사용자 로케일 조회 실패, 브라우저 언어 사용");
|
||||||
const browserLang = navigator.language.split("-")[0];
|
const browserLang = navigator.language.split("-")[0];
|
||||||
const langMapping: Record<string, string> = {
|
|
||||||
ko: "KR",
|
|
||||||
en: "US",
|
|
||||||
ja: "JP",
|
|
||||||
zh: "CN",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (langMapping[browserLang]) {
|
// 브라우저 언어를 그대로 사용 (매핑 없음)
|
||||||
setUserLang(langMapping[browserLang]);
|
if (["ko", "en", "ja", "zh"].includes(browserLang)) {
|
||||||
|
setUserLang(browserLang);
|
||||||
|
globalUserLang = browserLang;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ 사용자 로케일 조회 중 오류:", error);
|
console.error("❌ 사용자 로케일 조회 중 오류:", error);
|
||||||
|
|
||||||
// 오류 시 브라우저 언어 사용
|
// 오류 시 브라우저 언어 사용
|
||||||
const browserLang = navigator.language.split("-")[0];
|
const browserLang = navigator.language.split("-")[0];
|
||||||
const langMapping: Record<string, string> = {
|
|
||||||
ko: "KR",
|
|
||||||
en: "US",
|
|
||||||
ja: "JP",
|
|
||||||
zh: "CN",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (langMapping[browserLang]) {
|
// 브라우저 언어를 그대로 사용 (매핑 없음)
|
||||||
setUserLang(langMapping[browserLang]);
|
if (["ko", "en", "ja", "zh"].includes(browserLang)) {
|
||||||
|
setUserLang(browserLang);
|
||||||
|
globalUserLang = browserLang;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -90,42 +72,69 @@ export function useMultiLang(options: UseMultiLangOptions = {}) {
|
||||||
fetchUserLocale();
|
fetchUserLocale();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 다국어 텍스트 가져오기
|
// 다국어 텍스트 가져오기 (배치 조회 방식)
|
||||||
const getText = async (menuCode: string, langKey: string, fallback?: string): Promise<string> => {
|
const getText = async (menuCode: string, langKey: string, fallback?: string): Promise<string> => {
|
||||||
console.log(`🔍 다국어 텍스트 요청:`, { menuCode, langKey, userLang, companyCode });
|
console.log(`🔍 다국어 텍스트 요청 (배치 방식):`, { menuCode, langKey, userLang, companyCode });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `/multilang/user-text/${companyCode}/${menuCode}/${langKey}?userLang=${userLang}`;
|
// 배치 조회 API 사용
|
||||||
console.log(`📡 API 요청 URL:`, url);
|
const response = await apiClient.post(
|
||||||
|
"/multilang/batch",
|
||||||
|
{
|
||||||
|
langKeys: [langKey],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
companyCode,
|
||||||
|
menuCode,
|
||||||
|
userLang,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const response = await apiClient.get(url);
|
console.log(`📡 배치 API 응답 상태:`, response.status, response.statusText);
|
||||||
|
|
||||||
console.log(`📡 API 응답 상태:`, response.status, response.statusText);
|
if (response.data.success && response.data.data && response.data.data[langKey]) {
|
||||||
|
// 번역 텍스트를 캐시에 저장
|
||||||
if (response.data.success && response.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] = response.data.data;
|
currentCache[cacheKey] = response.data.data[langKey];
|
||||||
(window as any).__TRANSLATION_CACHE = currentCache;
|
(window as any).__TRANSLATION_CACHE = currentCache;
|
||||||
|
|
||||||
return response.data.data;
|
return response.data.data[langKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 실패 시 fallback 또는 키 반환
|
// 실패 시 fallback 또는 키 반환
|
||||||
console.log(`🔄 API 성공했지만 데이터 없음, fallback 반환:`, fallback || langKey);
|
console.log(`🔄 배치 API 성공했지만 데이터 없음, fallback 반환:`, fallback || langKey);
|
||||||
return fallback || langKey;
|
return fallback || langKey;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ 다국어 텍스트 조회 실패:", error);
|
console.error("❌ 다국어 텍스트 배치 조회 실패:", error);
|
||||||
console.log(`🔄 에러 시 fallback 반환:`, fallback || langKey);
|
console.log(`🔄 에러 시 fallback 반환:`, fallback || langKey);
|
||||||
return fallback || langKey;
|
return fallback || langKey;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 언어 변경
|
// 언어 변경
|
||||||
const changeLang = (newLang: string) => {
|
const changeLang = async (newLang: string) => {
|
||||||
setUserLang(newLang);
|
try {
|
||||||
globalUserLang = newLang;
|
// 백엔드에 사용자 로케일 설정 요청
|
||||||
|
const response = await apiClient.post("/admin/user-locale", {
|
||||||
|
locale: newLang,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
setUserLang(newLang);
|
||||||
|
globalUserLang = newLang;
|
||||||
|
console.log("✅ 사용자 로케일 변경 성공:", newLang);
|
||||||
|
} else {
|
||||||
|
console.error("❌ 사용자 로케일 변경 실패:", response.data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 사용자 로케일 변경 중 오류:", error);
|
||||||
|
// 오류 시에도 로컬 상태는 변경
|
||||||
|
setUserLang(newLang);
|
||||||
|
globalUserLang = newLang;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 전역 언어 상태 접근자
|
// 전역 언어 상태 접근자
|
||||||
|
|
@ -142,4 +151,4 @@ export function useMultiLang(options: UseMultiLangOptions = {}) {
|
||||||
getGlobalUserLang,
|
getGlobalUserLang,
|
||||||
setGlobalChangeLangCallback,
|
setGlobalChangeLangCallback,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import axios, { AxiosResponse, AxiosError } from "axios";
|
import axios, { AxiosResponse, AxiosError } from "axios";
|
||||||
|
|
||||||
// API 기본 URL 설정
|
// API 기본 URL 설정
|
||||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080/api";
|
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
|
||||||
|
|
||||||
// JWT 토큰 관리 유틸리티
|
// JWT 토큰 관리 유틸리티
|
||||||
const TokenManager = {
|
const TokenManager = {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Company, CompanyFormData } from "@/types/company";
|
import { Company, CompanyFormData } from "@/types/company";
|
||||||
|
import { apiClient } from "./client";
|
||||||
|
|
||||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "/api";
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "/api";
|
||||||
|
|
||||||
|
|
@ -15,31 +16,6 @@ interface ApiResponse<T = any> {
|
||||||
errorCode?: string;
|
errorCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 호출 헬퍼 함수
|
|
||||||
async function apiCall<T = any>(endpoint: string, options: RequestInit = {}): Promise<ApiResponse<T>> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
||||||
credentials: "include", // 세션 쿠키를 포함
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.message || `HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`API Error [${endpoint}]:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회사 목록 조회
|
* 회사 목록 조회
|
||||||
*/
|
*/
|
||||||
|
|
@ -54,28 +30,37 @@ export async function getCompanyList(params?: { company_name?: string; status?:
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryString = searchParams.toString();
|
const queryString = searchParams.toString();
|
||||||
const endpoint = `/admin/companies${queryString ? `?${queryString}` : ""}`;
|
// 실제 데이터베이스에서 회사 목록 조회하는 엔드포인트 사용
|
||||||
|
const endpoint = `/admin/companies/db${queryString ? `?${queryString}` : ""}`;
|
||||||
|
|
||||||
const response = await apiCall<Company[]>(endpoint);
|
console.log("🔍 실제 DB에서 회사 목록 조회 API 호출:", endpoint);
|
||||||
|
|
||||||
if (response.success && response.data) {
|
try {
|
||||||
return response.data;
|
const response = await apiClient.get(endpoint);
|
||||||
|
|
||||||
|
if (response.data.success && response.data.data) {
|
||||||
|
console.log("✅ 실제 DB에서 회사 목록 조회 성공:", response.data.data.length, "개");
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(response.data.message || "회사 목록 조회에 실패했습니다.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 실제 DB에서 회사 목록 조회 실패:", error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(response.message || "회사 목록 조회에 실패했습니다.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회사 단건 조회
|
* 회사 단건 조회
|
||||||
*/
|
*/
|
||||||
export async function getCompanyInfo(companyCode: string): Promise<Company> {
|
export async function getCompanyInfo(companyCode: string): Promise<Company> {
|
||||||
const response = await apiCall<Company>(`/admin/companies/${companyCode}`);
|
const response = await apiClient.get(`/admin/companies/${companyCode}`);
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.data.success && response.data.data) {
|
||||||
return response.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(response.message || "회사 정보 조회에 실패했습니다.");
|
throw new Error(response.data.message || "회사 정보 조회에 실패했습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -84,22 +69,19 @@ export async function getCompanyInfo(companyCode: string): Promise<Company> {
|
||||||
export async function createCompany(formData: CompanyFormData): Promise<Company> {
|
export async function createCompany(formData: CompanyFormData): Promise<Company> {
|
||||||
console.log("회사 등록 요청:", formData);
|
console.log("회사 등록 요청:", formData);
|
||||||
|
|
||||||
const response = await apiCall<Company>("/admin/companies", {
|
const response = await apiClient.post("/admin/companies", formData);
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(formData),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.data.success && response.data.data) {
|
||||||
console.log("회사 등록 완료:", {
|
console.log("회사 등록 완료:", {
|
||||||
code: response.data.company_code,
|
code: response.data.data.company_code,
|
||||||
name: response.data.company_name,
|
name: response.data.data.company_name,
|
||||||
writer: response.data.writer,
|
writer: response.data.data.writer,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(response.message || "회사 등록에 실패했습니다.");
|
throw new Error(response.data.message || "회사 등록에 실패했습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -109,28 +91,23 @@ export async function updateCompany(
|
||||||
companyCode: string,
|
companyCode: string,
|
||||||
formData: Partial<CompanyFormData> & { status?: string },
|
formData: Partial<CompanyFormData> & { status?: string },
|
||||||
): Promise<Company> {
|
): Promise<Company> {
|
||||||
const response = await apiCall<Company>(`/admin/companies/${companyCode}`, {
|
const response = await apiClient.put(`/admin/companies/${companyCode}`, formData);
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify(formData),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.data.success && response.data.data) {
|
||||||
return response.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(response.message || "회사 정보 수정에 실패했습니다.");
|
throw new Error(response.data.message || "회사 정보 수정에 실패했습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회사 삭제
|
* 회사 삭제
|
||||||
*/
|
*/
|
||||||
export async function deleteCompany(companyCode: string): Promise<void> {
|
export async function deleteCompany(companyCode: string): Promise<void> {
|
||||||
const response = await apiCall(`/admin/companies/${companyCode}`, {
|
const response = await apiClient.delete(`/admin/companies/${companyCode}`);
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.data.success) {
|
||||||
throw new Error(response.message || "회사 삭제에 실패했습니다.");
|
throw new Error(response.data.message || "회사 삭제에 실패했습니다.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,12 @@ export const menuApi = {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 메뉴 수정
|
||||||
|
updateMenu: async (menuId: string, menuData: MenuFormData): Promise<ApiResponse<void>> => {
|
||||||
|
const response = await apiClient.put(`/admin/menus/${menuId}`, menuData);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
// 메뉴 삭제
|
// 메뉴 삭제
|
||||||
deleteMenu: async (menuId: string): Promise<ApiResponse<void>> => {
|
deleteMenu: async (menuId: string): Promise<ApiResponse<void>> => {
|
||||||
const response = await apiClient.delete(`/admin/menus/${menuId}`);
|
const response = await apiClient.delete(`/admin/menus/${menuId}`);
|
||||||
|
|
@ -139,7 +145,16 @@ export const menuApi = {
|
||||||
menuCode?: string;
|
menuCode?: string;
|
||||||
keyType?: string;
|
keyType?: string;
|
||||||
}): Promise<ApiResponse<LangKey[]>> => {
|
}): Promise<ApiResponse<LangKey[]>> => {
|
||||||
const response = await apiClient.get("/multilang/keys", { params });
|
console.log("🔍 다국어 키 목록 조회 API 호출:", "/admin/multilang/keys", params);
|
||||||
return response.data;
|
|
||||||
|
try {
|
||||||
|
// Node.js 백엔드의 실제 라우팅과 일치하도록 수정
|
||||||
|
const response = await apiClient.get("/admin/multilang/keys", { params });
|
||||||
|
console.log("✅ 다국어 키 목록 조회 성공:", response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 다국어 키 목록 조회 실패:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,9 @@ export async function getDepartmentList(companyCode?: string) {
|
||||||
const response = await apiClient.get(`/admin/departments${params}`);
|
const response = await apiClient.get(`/admin/departments${params}`);
|
||||||
|
|
||||||
if (response.data.success && response.data.data) {
|
if (response.data.success && response.data.data) {
|
||||||
return response.data.data;
|
// 백엔드 API 응답 구조: { data: { departments: [], flatList: [] } }
|
||||||
|
// departments 배열을 반환 (트리 구조)
|
||||||
|
return response.data.data.departments || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(response.data.message || "부서 목록 조회에 실패했습니다.");
|
throw new Error(response.data.message || "부서 목록 조회에 실패했습니다.");
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,22 @@
|
||||||
import { useMultiLang } from "@/hooks/useMultiLang";
|
|
||||||
import { apiClient } from "../api/client";
|
import { apiClient } from "../api/client";
|
||||||
|
|
||||||
// 메뉴 관리 화면 다국어 키 상수
|
// 메뉴 관리 화면 다국어 키 상수
|
||||||
export const MENU_MANAGEMENT_KEYS = {
|
export const MENU_MANAGEMENT_KEYS = {
|
||||||
// 메뉴 타입 관련
|
// 기본 정보
|
||||||
TITLE: "menu.management.title",
|
TITLE: "title",
|
||||||
DESCRIPTION: "menu.management.description",
|
DESCRIPTION: "description",
|
||||||
MENU_TYPE_TITLE: "menu.type.title",
|
MENU_TYPE_TITLE: "menu.type.title",
|
||||||
ADMIN_MENU: "menu.management.admin",
|
|
||||||
USER_MENU: "menu.management.user",
|
|
||||||
ADMIN_DESCRIPTION: "menu.management.admin.description",
|
|
||||||
USER_DESCRIPTION: "menu.management.user.description",
|
|
||||||
MENU_TYPE_ADMIN: "menu.type.admin",
|
MENU_TYPE_ADMIN: "menu.type.admin",
|
||||||
MENU_TYPE_USER: "menu.type.user",
|
MENU_TYPE_USER: "menu.type.user",
|
||||||
|
ADMIN_MENU: "admin.menu",
|
||||||
|
USER_MENU: "user.menu",
|
||||||
|
ADMIN_DESCRIPTION: "admin.description",
|
||||||
|
USER_DESCRIPTION: "user.description",
|
||||||
|
LIST_TITLE: "list.title",
|
||||||
|
LIST_TOTAL: "list.total",
|
||||||
|
LIST_SEARCH_RESULT: "list.search.result",
|
||||||
|
|
||||||
// 메뉴 목록 관련
|
// 필터 관련
|
||||||
LIST_TITLE: "menu.list.title",
|
|
||||||
LIST_TOTAL: "menu.list.total",
|
|
||||||
LIST_SEARCH_RESULT: "menu.list.search.result",
|
|
||||||
|
|
||||||
// 필터 및 검색 관련
|
|
||||||
FILTER_COMPANY: "filter.company",
|
FILTER_COMPANY: "filter.company",
|
||||||
FILTER_COMPANY_ALL: "filter.company.all",
|
FILTER_COMPANY_ALL: "filter.company.all",
|
||||||
FILTER_COMPANY_COMMON: "filter.company.common",
|
FILTER_COMPANY_COMMON: "filter.company.common",
|
||||||
|
|
@ -43,7 +40,7 @@ export const MENU_MANAGEMENT_KEYS = {
|
||||||
BUTTON_REGISTER: "button.register",
|
BUTTON_REGISTER: "button.register",
|
||||||
BUTTON_MODIFY: "button.modify",
|
BUTTON_MODIFY: "button.modify",
|
||||||
|
|
||||||
// 메뉴 폼 관련
|
// 폼 관련
|
||||||
FORM_MENU_TYPE: "form.menu.type",
|
FORM_MENU_TYPE: "form.menu.type",
|
||||||
FORM_MENU_TYPE_ADMIN: "form.menu.type.admin",
|
FORM_MENU_TYPE_ADMIN: "form.menu.type.admin",
|
||||||
FORM_MENU_TYPE_USER: "form.menu.type.user",
|
FORM_MENU_TYPE_USER: "form.menu.type.user",
|
||||||
|
|
@ -115,215 +112,172 @@ export const MENU_MANAGEMENT_KEYS = {
|
||||||
UI_LANGUAGE: "ui.language",
|
UI_LANGUAGE: "ui.language",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// 다국어 텍스트 가져오기 함수
|
// 다국어 텍스트 캐시 (메모리 기반)
|
||||||
export const useMenuManagementText = () => {
|
const translationCache: Record<string, Record<string, string>> = {};
|
||||||
const { getText } = useMultiLang({ companyCode: "*" });
|
|
||||||
|
|
||||||
const getMenuText = async (key: string, params?: Record<string, string | number>): Promise<string> => {
|
// 배치 조회를 위한 키 수집기
|
||||||
let text = await getText("MENU_MANAGEMENT", key);
|
const pendingKeys: Set<string> = new Set();
|
||||||
|
let batchTimeout: NodeJS.Timeout | null = null;
|
||||||
|
const BATCH_DELAY = 50; // 50ms 지연으로 배치 처리
|
||||||
|
|
||||||
// 파라미터 치환
|
/**
|
||||||
if (params) {
|
* 다국어 텍스트 배치 조회
|
||||||
Object.entries(params).forEach(([paramKey, paramValue]) => {
|
* 여러 키를 한번에 조회하여 API 호출 횟수를 대폭 줄임
|
||||||
text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), paramValue.toString());
|
*/
|
||||||
});
|
async function fetchBatchTranslations(
|
||||||
|
keys: string[],
|
||||||
|
companyCode: string = "*",
|
||||||
|
menuCode: string = "MENU_MANAGEMENT",
|
||||||
|
userLang: string = "KR",
|
||||||
|
): Promise<Record<string, string>> {
|
||||||
|
try {
|
||||||
|
console.log(`🚀 배치 조회 시작: ${keys.length}개 키`);
|
||||||
|
|
||||||
|
const response = await apiClient.post(
|
||||||
|
"/multilang/batch",
|
||||||
|
{
|
||||||
|
langKeys: keys,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
companyCode,
|
||||||
|
menuCode,
|
||||||
|
userLang,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
console.log(`✅ 배치 조회 성공: ${keys.length}개 키`);
|
||||||
|
return response.data.data || {};
|
||||||
|
} else {
|
||||||
|
console.error("❌ 배치 조회 실패:", response.data.message);
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 배치 조회 오류:", error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return text;
|
/**
|
||||||
};
|
* 개별 다국어 텍스트 조회 (배치 처리)
|
||||||
|
* 실제로는 배치로 처리되어 API 호출 횟수가 대폭 감소
|
||||||
return {
|
*/
|
||||||
getMenuText,
|
export async function getMultilangText(
|
||||||
keys: MENU_MANAGEMENT_KEYS,
|
key: string,
|
||||||
};
|
companyCode: string = "*",
|
||||||
};
|
menuCode: string = "MENU_MANAGEMENT",
|
||||||
|
userLang: string = "KR",
|
||||||
// 전역 번역 캐시
|
): Promise<string> {
|
||||||
let translationCache: Record<string, Record<string, string>> = {};
|
// 1. 캐시에서 먼저 확인
|
||||||
|
const cacheKey = `${userLang}_${companyCode}_${menuCode}`;
|
||||||
// 번역 캐시 설정 함수
|
if (translationCache[cacheKey]?.[key]) {
|
||||||
export const setTranslationCache = (lang: string, translations: Record<string, string>) => {
|
return translationCache[cacheKey][key];
|
||||||
translationCache[lang] = translations;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 번역 캐시 가져오기 함수
|
|
||||||
export const getTranslationCache = (lang: string): Record<string, string> => {
|
|
||||||
return translationCache[lang] || {};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 동기적 다국어 텍스트 가져오기 (캐시된 값 사용)
|
|
||||||
export const getMenuTextSync = (key: string, params?: Record<string, any>): string => {
|
|
||||||
// 전역 언어 상태 확인
|
|
||||||
const userLang = (typeof window !== "undefined" && (window as any).__GLOBAL_USER_LANG) || "KR";
|
|
||||||
|
|
||||||
// 현재 언어가 한국어가 아니면 캐시에서 번역 텍스트 찾기
|
|
||||||
if (userLang !== "KR") {
|
|
||||||
// 1. 먼저 전역 캐시에서 찾기
|
|
||||||
const cachedTranslations = getTranslationCache(userLang);
|
|
||||||
if (cachedTranslations[key]) {
|
|
||||||
let text = cachedTranslations[key];
|
|
||||||
|
|
||||||
// 파라미터 치환
|
|
||||||
if (params) {
|
|
||||||
Object.entries(params).forEach(([paramKey, paramValue]) => {
|
|
||||||
text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), paramValue.toString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 개별 캐시에서 찾기
|
|
||||||
const individualCache = (typeof window !== "undefined" && (window as any).__TRANSLATION_CACHE) || {};
|
|
||||||
const cacheKey = `MENU_MANAGEMENT.${key}`;
|
|
||||||
if (individualCache[cacheKey]) {
|
|
||||||
let text = individualCache[cacheKey];
|
|
||||||
|
|
||||||
// 파라미터 치환
|
|
||||||
if (params) {
|
|
||||||
Object.entries(params).forEach(([paramKey, paramValue]) => {
|
|
||||||
text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), paramValue.toString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ 개별 캐시에서 번역 사용:`, { key, result: text, userLang });
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 캐시에 없으면 비동기적으로 로드 시도
|
|
||||||
console.log(`⚠️ getMenuTextSync: 캐시에 번역이 없습니다. 키: ${key}, 언어: ${userLang}`);
|
|
||||||
|
|
||||||
// 비동기적으로 번역 로드 (백그라운드에서)
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const companyCode = "*";
|
|
||||||
|
|
||||||
apiClient
|
|
||||||
.get(`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${key}?userLang=${userLang}`)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.data.success && response.data.data) {
|
|
||||||
// 개별 캐시에 저장
|
|
||||||
const currentCache = (window as any).__TRANSLATION_CACHE || {};
|
|
||||||
currentCache[cacheKey] = data.data;
|
|
||||||
(window as any).__TRANSLATION_CACHE = currentCache;
|
|
||||||
|
|
||||||
// 전역 캐시에도 저장
|
|
||||||
const globalCache = getTranslationCache(userLang);
|
|
||||||
globalCache[key] = data.data;
|
|
||||||
setTranslationCache(userLang, globalCache);
|
|
||||||
|
|
||||||
// 페이지 리렌더링을 위해 이벤트 발생
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent("translation-loaded", {
|
|
||||||
detail: { key, text: data.data, userLang },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(`❌ 백그라운드 번역 로드 실패:`, { key, error });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 캐시에 없으면 기본 텍스트에서 찾기
|
|
||||||
const defaultTexts: Record<string, string> = {
|
|
||||||
[MENU_MANAGEMENT_KEYS.TITLE]: "메뉴 관리",
|
|
||||||
[MENU_MANAGEMENT_KEYS.DESCRIPTION]: "시스템의 메뉴 구조와 권한을 관리합니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MENU_TYPE_TITLE]: "메뉴 타입",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MENU_TYPE_ADMIN]: "관리자",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MENU_TYPE_USER]: "사용자",
|
|
||||||
[MENU_MANAGEMENT_KEYS.ADMIN_MENU]: "관리자 메뉴",
|
|
||||||
[MENU_MANAGEMENT_KEYS.USER_MENU]: "사용자 메뉴",
|
|
||||||
[MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION]: "시스템 관리 및 설정 메뉴",
|
|
||||||
[MENU_MANAGEMENT_KEYS.USER_DESCRIPTION]: "일반 사용자 업무 메뉴",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_ADD]: "추가",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL]: "최상위 메뉴 추가",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB]: "하위 메뉴 추가",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_EDIT]: "수정",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE]: "삭제",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED]: "선택 삭제",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED_COUNT]: "선택 삭제 ({count})",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE_PROCESSING]: "삭제 중...",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_CANCEL]: "취소",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_SAVE]: "저장",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_SAVE_PROCESSING]: "저장 중...",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_REGISTER]: "등록",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_MODIFY]: "수정",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE]: "메뉴 타입",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE_ADMIN]: "관리자",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE_USER]: "사용자",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_STATUS]: "상태",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_STATUS_ACTIVE]: "활성화",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_STATUS_INACTIVE]: "비활성화",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_COMPANY]: "회사",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_COMPANY_SELECT]: "회사를 선택하세요",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_COMPANY_COMMON]: "공통",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_COMPANY_SUBMENU_NOTE]: "하위 메뉴는 상위 메뉴와 동일한 회사를 가져야 합니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_MENU_NAME]: "메뉴명",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_MENU_NAME_PLACEHOLDER]: "메뉴명을 입력하세요",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_MENU_URL]: "URL",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_MENU_URL_PLACEHOLDER]: "메뉴 URL을 입력하세요",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION]: "설명",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION_PLACEHOLDER]: "메뉴 설명을 입력하세요",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_MENU_SEQUENCE]: "순서",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_LANG_KEY]: "다국어 키",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECT]: "다국어 키를 선택하세요",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_NONE]: "다국어 키 없음",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SEARCH]: "다국어 키 검색...",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECTED]: "선택된 키: {key} - {description}",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MODAL_MENU_REGISTER_TITLE]: "메뉴 등록",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MODAL_MENU_MODIFY_TITLE]: "메뉴 수정",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MODAL_DELETE_TITLE]: "메뉴 삭제",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MODAL_DELETE_DESCRIPTION]: "해당 메뉴를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MODAL_DELETE_BATCH_DESCRIPTION]:
|
|
||||||
"선택된 {count}개의 메뉴를 영구적으로 삭제하시겠습니까?\n\n⚠️ 주의: 상위 메뉴를 삭제하면 하위 메뉴들도 함께 삭제됩니다.\n이 작업은 되돌릴 수 없습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_SELECT]: "선택",
|
|
||||||
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_NAME]: "메뉴명",
|
|
||||||
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_URL]: "URL",
|
|
||||||
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_TYPE]: "메뉴 타입",
|
|
||||||
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_STATUS]: "상태",
|
|
||||||
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_COMPANY]: "회사",
|
|
||||||
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_SEQUENCE]: "순서",
|
|
||||||
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_ACTIONS]: "작업",
|
|
||||||
[MENU_MANAGEMENT_KEYS.STATUS_ACTIVE]: "활성화",
|
|
||||||
[MENU_MANAGEMENT_KEYS.STATUS_INACTIVE]: "비활성화",
|
|
||||||
[MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED]: "미지정",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_LOADING]: "로딩 중...",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_PROCESSING]: "메뉴 삭제 중...",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_SUCCESS]: "메뉴가 저장되었습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED]: "메뉴 저장에 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_SUCCESS]: "메뉴가 삭제되었습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_FAILED]: "메뉴 삭제에 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_SUCCESS]:
|
|
||||||
"✅ {count}개의 메뉴(및 하위 메뉴)가 성공적으로 삭제되었습니다!",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_PARTIAL]: "⚠️ {success}개 삭제 성공, {failed}개 삭제 실패",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_SUCCESS]: "메뉴 상태가 변경되었습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_FAILED]: "메뉴 상태 변경에 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_MENU_NAME_REQUIRED]: "메뉴명을 입력해주세요.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_COMPANY_REQUIRED]: "회사를 선택해주세요.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_SELECT_MENU_DELETE]: "삭제할 메뉴를 선택해주세요.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_LIST]: "메뉴 목록을 불러오는데 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_INFO]: "메뉴 정보를 불러오는데 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST]: "회사 목록을 불러오는데 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST]: "다국어 키 목록을 불러오는데 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.UI_EXPAND]: "펼치기",
|
|
||||||
[MENU_MANAGEMENT_KEYS.UI_COLLAPSE]: "접기",
|
|
||||||
[MENU_MANAGEMENT_KEYS.UI_MENU_COLLAPSE]: "메뉴 접기",
|
|
||||||
[MENU_MANAGEMENT_KEYS.UI_LANGUAGE]: "언어",
|
|
||||||
};
|
|
||||||
|
|
||||||
let text = defaultTexts[key] || key;
|
|
||||||
|
|
||||||
// 파라미터 치환
|
|
||||||
if (params) {
|
|
||||||
Object.entries(params).forEach(([paramKey, paramValue]) => {
|
|
||||||
text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), paramValue.toString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 한국어인 경우 기본 텍스트 반환
|
// 2. 기본 텍스트에서 확인
|
||||||
|
const defaultText = getDefaultText(key);
|
||||||
|
if (defaultText) {
|
||||||
|
return defaultText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 배치 처리에 추가
|
||||||
|
pendingKeys.add(key);
|
||||||
|
|
||||||
|
// 4. 배치 타임아웃 설정
|
||||||
|
if (batchTimeout) {
|
||||||
|
clearTimeout(batchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
batchTimeout = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const keysToFetch = Array.from(pendingKeys);
|
||||||
|
pendingKeys.clear();
|
||||||
|
|
||||||
|
if (keysToFetch.length > 0) {
|
||||||
|
const translations = await fetchBatchTranslations(keysToFetch, companyCode, menuCode, userLang);
|
||||||
|
|
||||||
|
// 캐시에 저장
|
||||||
|
if (!translationCache[cacheKey]) {
|
||||||
|
translationCache[cacheKey] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(translationCache[cacheKey], translations);
|
||||||
|
|
||||||
|
// 요청된 키에 대한 번역 반환
|
||||||
|
if (translations[key]) {
|
||||||
|
resolve(translations[key]);
|
||||||
|
} else {
|
||||||
|
resolve(defaultText || key);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(defaultText || key);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 배치 처리 오류:", error);
|
||||||
|
resolve(defaultText || key);
|
||||||
|
}
|
||||||
|
}, BATCH_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동기적 다국어 텍스트 조회 (캐시에서만)
|
||||||
|
* UI 렌더링 시 즉시 사용
|
||||||
|
*/
|
||||||
|
export function getMultilangTextSync(
|
||||||
|
key: string,
|
||||||
|
companyCode: string = "*",
|
||||||
|
menuCode: string = "MENU_MANAGEMENT",
|
||||||
|
userLang: string = "KR",
|
||||||
|
): string {
|
||||||
|
// 1. 캐시에서 확인
|
||||||
|
const cacheKey = `${userLang}_${companyCode}_${menuCode}`;
|
||||||
|
if (translationCache[cacheKey]?.[key]) {
|
||||||
|
return translationCache[cacheKey][key];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 기본 텍스트에서 확인
|
||||||
|
const defaultText = getDefaultText(key);
|
||||||
|
if (defaultText) {
|
||||||
|
return defaultText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 캐시에 없으면 비동기적으로 로드 (백그라운드)
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
getMultilangText(key, companyCode, menuCode, userLang).then((text) => {
|
||||||
|
// 페이지 리렌더링을 위해 이벤트 발생
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("translation-loaded", {
|
||||||
|
detail: { key, text, userLang },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultText || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 관리 관련 다국어 텍스트 조회 (배치 처리)
|
||||||
|
*/
|
||||||
|
export async function getMenuText(key: string, userLang: string = "KR"): Promise<string> {
|
||||||
|
return getMultilangText(key, "*", "MENU_MANAGEMENT", userLang);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 관리 관련 다국어 텍스트 동기 조회
|
||||||
|
*/
|
||||||
|
export function getMenuTextSync(key: string, userLang: string = "KR"): string {
|
||||||
|
return getMultilangTextSync(key, "*", "MENU_MANAGEMENT", userLang);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기본 텍스트 반환 (한국어)
|
||||||
|
*/
|
||||||
|
function getDefaultText(key: string): string {
|
||||||
const defaultTexts: Record<string, string> = {
|
const defaultTexts: Record<string, string> = {
|
||||||
[MENU_MANAGEMENT_KEYS.TITLE]: "메뉴 관리",
|
[MENU_MANAGEMENT_KEYS.TITLE]: "메뉴 관리",
|
||||||
[MENU_MANAGEMENT_KEYS.DESCRIPTION]: "시스템의 메뉴 구조와 권한을 관리합니다.",
|
[MENU_MANAGEMENT_KEYS.DESCRIPTION]: "시스템의 메뉴 구조와 권한을 관리합니다.",
|
||||||
|
|
@ -334,16 +288,6 @@ export const getMenuTextSync = (key: string, params?: Record<string, any>): stri
|
||||||
[MENU_MANAGEMENT_KEYS.USER_MENU]: "사용자 메뉴",
|
[MENU_MANAGEMENT_KEYS.USER_MENU]: "사용자 메뉴",
|
||||||
[MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION]: "시스템 관리 및 설정 메뉴",
|
[MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION]: "시스템 관리 및 설정 메뉴",
|
||||||
[MENU_MANAGEMENT_KEYS.USER_DESCRIPTION]: "일반 사용자 업무 메뉴",
|
[MENU_MANAGEMENT_KEYS.USER_DESCRIPTION]: "일반 사용자 업무 메뉴",
|
||||||
[MENU_MANAGEMENT_KEYS.LIST_TITLE]: "메뉴 목록",
|
|
||||||
[MENU_MANAGEMENT_KEYS.LIST_TOTAL]: "총 {count}개의 메뉴가 있습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.LIST_SEARCH_RESULT]: "검색 결과: {count}개",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FILTER_COMPANY]: "회사",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FILTER_COMPANY_ALL]: "전체 회사",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FILTER_COMPANY_COMMON]: "공통",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FILTER_COMPANY_SEARCH]: "회사명 검색...",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FILTER_SEARCH]: "검색어",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FILTER_SEARCH_PLACEHOLDER]: "메뉴명 또는 URL 검색",
|
|
||||||
[MENU_MANAGEMENT_KEYS.FILTER_RESET]: "초기화",
|
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_ADD]: "추가",
|
[MENU_MANAGEMENT_KEYS.BUTTON_ADD]: "추가",
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL]: "최상위 메뉴 추가",
|
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL]: "최상위 메뉴 추가",
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB]: "하위 메뉴 추가",
|
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB]: "하위 메뉴 추가",
|
||||||
|
|
@ -396,38 +340,44 @@ export const getMenuTextSync = (key: string, params?: Record<string, any>): stri
|
||||||
[MENU_MANAGEMENT_KEYS.STATUS_ACTIVE]: "활성화",
|
[MENU_MANAGEMENT_KEYS.STATUS_ACTIVE]: "활성화",
|
||||||
[MENU_MANAGEMENT_KEYS.STATUS_INACTIVE]: "비활성화",
|
[MENU_MANAGEMENT_KEYS.STATUS_INACTIVE]: "비활성화",
|
||||||
[MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED]: "미지정",
|
[MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED]: "미지정",
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_LOADING]: "로딩 중...",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_PROCESSING]: "메뉴 삭제 중...",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_SUCCESS]: "메뉴가 저장되었습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED]: "메뉴 저장에 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_SUCCESS]: "메뉴가 삭제되었습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_FAILED]: "메뉴 삭제에 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_SUCCESS]:
|
|
||||||
"✅ {count}개의 메뉴(및 하위 메뉴)가 성공적으로 삭제되었습니다!",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_PARTIAL]: "⚠️ {success}개 삭제 성공, {failed}개 삭제 실패",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_SUCCESS]: "메뉴 상태가 변경되었습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_FAILED]: "메뉴 상태 변경에 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_MENU_NAME_REQUIRED]: "메뉴명을 입력해주세요.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_COMPANY_REQUIRED]: "회사를 선택해주세요.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_SELECT_MENU_DELETE]: "삭제할 메뉴를 선택해주세요.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_LIST]: "메뉴 목록을 불러오는데 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_INFO]: "메뉴 정보를 불러오는데 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST]: "회사 목록을 불러오는데 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST]: "다국어 키 목록을 불러오는데 실패했습니다.",
|
|
||||||
[MENU_MANAGEMENT_KEYS.UI_EXPAND]: "펼치기",
|
|
||||||
[MENU_MANAGEMENT_KEYS.UI_COLLAPSE]: "접기",
|
|
||||||
[MENU_MANAGEMENT_KEYS.UI_MENU_COLLAPSE]: "메뉴 접기",
|
|
||||||
[MENU_MANAGEMENT_KEYS.UI_LANGUAGE]: "언어",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = defaultTexts[key] || key;
|
return defaultTexts[key] || key;
|
||||||
|
}
|
||||||
|
|
||||||
// 파라미터 치환
|
/**
|
||||||
if (params) {
|
* 번역 캐시 설정 함수
|
||||||
Object.entries(params).forEach(([paramKey, paramValue]) => {
|
*/
|
||||||
text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), paramValue.toString());
|
export const setTranslationCache = (lang: string, translations: Record<string, string>) => {
|
||||||
});
|
translationCache[lang] = translations;
|
||||||
}
|
};
|
||||||
|
|
||||||
return text;
|
/**
|
||||||
|
* 번역 캐시 가져오기 함수
|
||||||
|
*/
|
||||||
|
export const getTranslationCache = (lang: string): Record<string, string> => {
|
||||||
|
return translationCache[lang] || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 관리 다국어 텍스트 훅 (기존 코드와 호환)
|
||||||
|
*/
|
||||||
|
export const useMenuManagementText = () => {
|
||||||
|
const getMenuText = async (key: string, params?: Record<string, string | number>): Promise<string> => {
|
||||||
|
let text = await getMultilangText(key, "*", "MENU_MANAGEMENT", "KR");
|
||||||
|
|
||||||
|
// 파라미터 치환
|
||||||
|
if (params) {
|
||||||
|
Object.entries(params).forEach(([paramKey, paramValue]) => {
|
||||||
|
text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), paramValue.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getMenuText,
|
||||||
|
keys: MENU_MANAGEMENT_KEYS,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,43 +2,35 @@
|
||||||
* 사용자 관리 관련 타입 정의
|
* 사용자 관리 관련 타입 정의
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 사용자 정보 인터페이스 (프론트엔드 친화적 camelCase)
|
// 사용자 정보 인터페이스 (백엔드 API 응답과 일치하는 camelCase)
|
||||||
export interface User {
|
export interface User {
|
||||||
sabun: string; // 사번
|
sabun?: string; // 사번
|
||||||
user_id: string; // 사용자 ID
|
userId: string; // 사용자 ID
|
||||||
user_name: string; // 사용자명
|
userName: string; // 사용자명
|
||||||
user_name_eng?: string; // 영문명
|
userNameEng?: string; // 영문명
|
||||||
user_name_cn?: string; // 중문명
|
userNameCn?: string; // 중문명
|
||||||
company_name?: string; // 회사명
|
companyCode?: string; // 회사 코드
|
||||||
dept_code: string; // 부서 코드
|
companyName?: string; // 회사명
|
||||||
dept_name: string; // 부서명
|
deptCode?: string; // 부서 코드
|
||||||
position_code?: string; // 직책 코드
|
deptName?: string; // 부서명
|
||||||
position_name: string; // 직책
|
positionCode?: string; // 직책 코드
|
||||||
email: string; // 이메일
|
positionName?: string; // 직책
|
||||||
tel: string; // 전화번호
|
email?: string; // 이메일
|
||||||
cell_phone: string; // 휴대폰
|
tel?: string; // 전화번호
|
||||||
user_type?: string; // 사용자 유형 코드
|
cellPhone?: string; // 휴대폰
|
||||||
user_type_name: string; // 사용자 유형명
|
userType?: string; // 사용자 유형 코드
|
||||||
regdate: string; // 등록일 (YYYY-MM-DD)
|
userTypeName?: string; // 사용자 유형명
|
||||||
regdate_org?: string; // 원본 등록일
|
regDate?: string; // 등록일 (YYYY-MM-DD)
|
||||||
status: string; // 상태 (active, inactive)
|
status: string; // 상태 (active, inactive)
|
||||||
data_type?: string; // 데이터 타입
|
dataType?: string; // 데이터 타입
|
||||||
enddate?: string; // 퇴사일
|
endDate?: string; // 퇴사일
|
||||||
|
locale?: string; // 로케일
|
||||||
rnum?: number; // 행 번호
|
rnum?: number; // 행 번호
|
||||||
}
|
}
|
||||||
|
|
||||||
// 사용자 검색 필터
|
// 사용자 검색 필터
|
||||||
export interface UserSearchFilter {
|
export interface UserSearchFilter {
|
||||||
searchType?:
|
searchType?: "all" | "sabun" | "companyCode" | "deptName" | "positionName" | "userId" | "userName" | "tel" | "email"; // 검색 대상
|
||||||
| "all"
|
|
||||||
| "sabun"
|
|
||||||
| "company_name"
|
|
||||||
| "dept_name"
|
|
||||||
| "position_name"
|
|
||||||
| "user_id"
|
|
||||||
| "user_name"
|
|
||||||
| "tel"
|
|
||||||
| "email"; // 검색 대상
|
|
||||||
searchValue?: string; // 검색어
|
searchValue?: string; // 검색어
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,15 +44,15 @@ export interface UserTableColumn {
|
||||||
|
|
||||||
// 사용자 등록/수정 폼 데이터
|
// 사용자 등록/수정 폼 데이터
|
||||||
export interface UserFormData {
|
export interface UserFormData {
|
||||||
user_id: string;
|
userId: string;
|
||||||
user_name: string;
|
userName: string;
|
||||||
dept_code: string;
|
deptCode: string;
|
||||||
dept_name: string;
|
deptName: string;
|
||||||
position_name: string;
|
positionName: string;
|
||||||
email: string;
|
email: string;
|
||||||
tel: string;
|
tel: string;
|
||||||
cell_phone: string;
|
cellPhone: string;
|
||||||
user_type_name: string;
|
userTypeName: string;
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue