사용자관리 등록
This commit is contained in:
parent
8667cb4780
commit
ce130ee225
|
|
@ -4,6 +4,7 @@ import { AuthenticatedRequest } from "../types/auth";
|
|||
import { ApiResponse } from "../types/common";
|
||||
import { Client } from "pg";
|
||||
import { AdminService } from "../services/adminService";
|
||||
import { EncryptUtil } from "../utils/encryptUtil";
|
||||
|
||||
/**
|
||||
* 관리자 메뉴 목록 조회
|
||||
|
|
@ -172,67 +173,215 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
|||
user: req.user,
|
||||
});
|
||||
|
||||
// 임시 더미 데이터 반환 (실제로는 데이터베이스에서 조회)
|
||||
const { page = 1, countPerPage = 20 } = req.query;
|
||||
const { page = 1, countPerPage = 20, search, deptCode, status } = req.query;
|
||||
|
||||
const dummyUsers = [
|
||||
{
|
||||
userId: "plm_admin",
|
||||
userName: "관리자",
|
||||
deptName: "IT팀",
|
||||
companyCode: "ILSHIN",
|
||||
userType: "admin",
|
||||
email: "admin@ilshin.com",
|
||||
status: "active",
|
||||
regDate: "2024-01-15",
|
||||
},
|
||||
{
|
||||
userId: "user001",
|
||||
userName: "홍길동",
|
||||
deptName: "영업팀",
|
||||
companyCode: "ILSHIN",
|
||||
userType: "user",
|
||||
email: "hong@ilshin.com",
|
||||
status: "active",
|
||||
regDate: "2024-01-16",
|
||||
},
|
||||
{
|
||||
userId: "user002",
|
||||
userName: "김철수",
|
||||
deptName: "개발팀",
|
||||
companyCode: "ILSHIN",
|
||||
userType: "user",
|
||||
email: "kim@ilshin.com",
|
||||
status: "inactive",
|
||||
regDate: "2024-01-17",
|
||||
},
|
||||
];
|
||||
// PostgreSQL 클라이언트 생성
|
||||
const client = new Client({
|
||||
connectionString:
|
||||
process.env.DATABASE_URL ||
|
||||
"postgresql://postgres:postgres@localhost:5432/ilshin",
|
||||
});
|
||||
|
||||
// 페이징 처리
|
||||
const startIndex = (Number(page) - 1) * Number(countPerPage);
|
||||
const endIndex = startIndex + Number(countPerPage);
|
||||
const paginatedUsers = dummyUsers.slice(startIndex, endIndex);
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
// 기본 사용자 목록 조회 쿼리
|
||||
let query = `
|
||||
SELECT
|
||||
u.sabun,
|
||||
u.user_id,
|
||||
u.user_name,
|
||||
u.user_name_eng,
|
||||
u.dept_code,
|
||||
u.dept_name,
|
||||
u.position_code,
|
||||
u.position_name,
|
||||
u.email,
|
||||
u.tel,
|
||||
u.cell_phone,
|
||||
u.user_type,
|
||||
u.user_type_name,
|
||||
u.regdate,
|
||||
u.status,
|
||||
u.company_code,
|
||||
u.locale,
|
||||
d.dept_name as dept_name_full
|
||||
FROM user_info u
|
||||
LEFT JOIN dept_info d ON u.dept_code = d.dept_code
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const queryParams: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 검색 조건 추가
|
||||
if (search) {
|
||||
query += ` AND (
|
||||
u.user_name ILIKE $${paramIndex} OR
|
||||
u.user_id ILIKE $${paramIndex} OR
|
||||
u.sabun ILIKE $${paramIndex} OR
|
||||
u.email ILIKE $${paramIndex}
|
||||
)`;
|
||||
queryParams.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 부서 코드 필터
|
||||
if (deptCode) {
|
||||
query += ` AND u.dept_code = $${paramIndex}`;
|
||||
queryParams.push(deptCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (status) {
|
||||
query += ` AND u.status = $${paramIndex}`;
|
||||
queryParams.push(status);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 정렬
|
||||
query += ` ORDER BY u.regdate DESC, u.user_name`;
|
||||
|
||||
// 총 개수 조회
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM user_info u
|
||||
WHERE 1=1
|
||||
${
|
||||
search
|
||||
? `AND (
|
||||
u.user_name ILIKE $1 OR
|
||||
u.user_id ILIKE $1 OR
|
||||
u.sabun ILIKE $1 OR
|
||||
u.email ILIKE $1
|
||||
)`
|
||||
: ""
|
||||
}
|
||||
${deptCode ? `AND u.dept_code = $${search ? 2 : 1}` : ""}
|
||||
${status ? `AND u.status = $${search ? (deptCode ? 3 : 2) : deptCode ? 2 : 1}` : ""}
|
||||
`;
|
||||
|
||||
const countParams = queryParams.slice(0, -2); // 페이징 파라미터 제외
|
||||
const countResult = await client.query(countQuery, countParams);
|
||||
const totalCount = parseInt(countResult.rows[0].total);
|
||||
|
||||
// 페이징 적용
|
||||
const offset = (Number(page) - 1) * Number(countPerPage);
|
||||
query += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
||||
queryParams.push(Number(countPerPage), offset);
|
||||
|
||||
// 사용자 목록 조회
|
||||
const result = await client.query(query, queryParams);
|
||||
const users = result.rows;
|
||||
|
||||
// 디버깅: 원본 데이터베이스 조회 결과 로깅
|
||||
logger.info("=== 원본 데이터베이스 조회 결과 ===");
|
||||
logger.info("총 조회된 사용자 수:", users.length);
|
||||
if (users.length > 0) {
|
||||
logger.info("첫 번째 사용자 원본 데이터:", {
|
||||
user_id: users[0].user_id,
|
||||
user_name: users[0].user_name,
|
||||
user_name_eng: users[0].user_name_eng,
|
||||
dept_code: users[0].dept_code,
|
||||
dept_name: users[0].dept_name,
|
||||
position_code: users[0].position_code,
|
||||
position_name: users[0].position_name,
|
||||
email: users[0].email,
|
||||
tel: users[0].tel,
|
||||
cell_phone: users[0].cell_phone,
|
||||
user_type: users[0].user_type,
|
||||
user_type_name: users[0].user_type_name,
|
||||
status: users[0].status,
|
||||
company_code: users[0].company_code,
|
||||
locale: users[0].locale,
|
||||
regdate: users[0].regdate,
|
||||
});
|
||||
}
|
||||
|
||||
// 응답 데이터 가공
|
||||
const processedUsers = users.map((user) => {
|
||||
// 디버깅: 각 필드별 값 확인
|
||||
const processedUser = {
|
||||
userId: user.user_id,
|
||||
userName: user.user_name,
|
||||
userNameEng: user.user_name_eng || null,
|
||||
sabun: user.sabun || null,
|
||||
deptCode: user.dept_code || null,
|
||||
deptName: user.dept_name || user.dept_name_full || null,
|
||||
positionCode: user.position_code || null,
|
||||
positionName: user.position_name || null,
|
||||
email: user.email || null,
|
||||
tel: user.tel || null,
|
||||
cellPhone: user.cell_phone || null,
|
||||
userType: user.user_type || null,
|
||||
userTypeName: user.user_type_name || null,
|
||||
status: user.status || "active",
|
||||
companyCode: user.company_code || null,
|
||||
locale: user.locale || null,
|
||||
regDate: user.regdate
|
||||
? user.regdate.toISOString().split("T")[0]
|
||||
: null,
|
||||
};
|
||||
|
||||
// 빈 문자열을 null로 변환
|
||||
Object.keys(processedUser).forEach((key) => {
|
||||
if (processedUser[key as keyof typeof processedUser] === "") {
|
||||
(processedUser as any)[key] = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 디버깅: 가공된 데이터 로깅
|
||||
logger.info(`사용자 ${user.user_id} 가공 결과:`, {
|
||||
원본: {
|
||||
user_id: user.user_id,
|
||||
user_name: user.user_name,
|
||||
user_name_eng: user.user_name_eng,
|
||||
dept_code: user.dept_code,
|
||||
dept_name: user.dept_name,
|
||||
position_code: user.position_code,
|
||||
position_name: user.position_name,
|
||||
email: user.email,
|
||||
tel: user.tel,
|
||||
cell_phone: user.cell_phone,
|
||||
user_type: user.user_type,
|
||||
user_type_name: user.user_type_name,
|
||||
status: user.status,
|
||||
company_code: user.company_code,
|
||||
locale: user.locale,
|
||||
regdate: user.regdate,
|
||||
},
|
||||
가공후: processedUser,
|
||||
});
|
||||
|
||||
return processedUser;
|
||||
});
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
data: {
|
||||
users: paginatedUsers,
|
||||
users: processedUsers,
|
||||
pagination: {
|
||||
currentPage: Number(page),
|
||||
countPerPage: Number(countPerPage),
|
||||
totalCount: dummyUsers.length,
|
||||
totalPages: Math.ceil(dummyUsers.length / Number(countPerPage)),
|
||||
totalCount: totalCount,
|
||||
totalPages: Math.ceil(totalCount / Number(countPerPage)),
|
||||
},
|
||||
},
|
||||
message: "사용자 목록 조회 성공",
|
||||
};
|
||||
|
||||
logger.info("사용자 목록 조회 성공", {
|
||||
totalCount: dummyUsers.length,
|
||||
returnedCount: paginatedUsers.length,
|
||||
totalCount,
|
||||
returnedCount: processedUsers.length,
|
||||
currentPage: Number(page),
|
||||
countPerPage: Number(countPerPage),
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("사용자 목록 조회 실패", { error });
|
||||
res.status(500).json({
|
||||
|
|
@ -386,7 +535,9 @@ export const setUserLocale = async (
|
|||
};
|
||||
|
||||
/**
|
||||
* GET /api/admin/companies
|
||||
* 회사 목록 조회 API
|
||||
* 기존 Java AdminController의 회사 목록 조회 기능 포팅
|
||||
*/
|
||||
export const getCompanyList = async (
|
||||
req: AuthenticatedRequest,
|
||||
|
|
@ -398,44 +549,61 @@ export const getCompanyList = async (
|
|||
user: req.user,
|
||||
});
|
||||
|
||||
// 임시 더미 데이터 반환 (실제로는 데이터베이스에서 조회)
|
||||
const dummyCompanies = [
|
||||
{
|
||||
company_code: "ILSHIN",
|
||||
company_name: "일신제강",
|
||||
status: "active",
|
||||
writer: "admin",
|
||||
regdate: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
company_code: "HUTECH",
|
||||
company_name: "후테크",
|
||||
status: "active",
|
||||
writer: "admin",
|
||||
regdate: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
company_code: "DAIN",
|
||||
company_name: "다인",
|
||||
status: "active",
|
||||
writer: "admin",
|
||||
regdate: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
// PostgreSQL 클라이언트 생성
|
||||
const client = new Client({
|
||||
connectionString:
|
||||
process.env.DATABASE_URL ||
|
||||
"postgresql://postgres:postgres@localhost:5432/ilshin",
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
// 회사 목록 조회 쿼리
|
||||
const query = `
|
||||
SELECT
|
||||
company_code,
|
||||
company_name,
|
||||
status,
|
||||
writer,
|
||||
regdate,
|
||||
'company' as data_type
|
||||
FROM company_mng
|
||||
WHERE status = 'active' OR status IS NULL
|
||||
ORDER BY company_name
|
||||
`;
|
||||
|
||||
const result = await client.query(query);
|
||||
const companies = result.rows;
|
||||
|
||||
// 프론트엔드에서 기대하는 응답 형식으로 변환
|
||||
const response = {
|
||||
success: true,
|
||||
data: dummyCompanies,
|
||||
data: companies.map((company) => ({
|
||||
company_code: company.company_code,
|
||||
company_name: company.company_name,
|
||||
status: company.status || "active",
|
||||
writer: company.writer,
|
||||
regdate: company.regdate
|
||||
? company.regdate.toISOString()
|
||||
: new Date().toISOString(),
|
||||
data_type: company.data_type,
|
||||
})),
|
||||
message: "회사 목록 조회 성공",
|
||||
};
|
||||
|
||||
logger.info("회사 목록 조회 성공", {
|
||||
totalCount: dummyCompanies.length,
|
||||
response: response,
|
||||
totalCount: companies.length,
|
||||
companies: companies.map((c) => ({
|
||||
code: c.company_code,
|
||||
name: c.company_name,
|
||||
})),
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("회사 목록 조회 실패", { error });
|
||||
res.status(500).json({
|
||||
|
|
@ -1259,3 +1427,552 @@ export async function getCompanyListFromDB(
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/admin/departments
|
||||
* 부서 목록 조회 API
|
||||
* 기존 Java AdminController의 부서 목록 조회 기능 포팅
|
||||
*/
|
||||
export const getDepartmentList = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
logger.info("부서 목록 조회 요청", {
|
||||
query: req.query,
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
const { companyCode, status, search } = req.query;
|
||||
|
||||
// PostgreSQL 클라이언트 생성
|
||||
const client = new Client({
|
||||
connectionString:
|
||||
process.env.DATABASE_URL ||
|
||||
"postgresql://postgres:postgres@localhost:5432/ilshin",
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
// 부서 목록 조회 쿼리
|
||||
let query = `
|
||||
SELECT
|
||||
dept_code,
|
||||
parent_dept_code,
|
||||
dept_name,
|
||||
master_sabun,
|
||||
master_user_id,
|
||||
location,
|
||||
location_name,
|
||||
regdate,
|
||||
data_type,
|
||||
status,
|
||||
sales_yn,
|
||||
company_name
|
||||
FROM dept_info
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const queryParams: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 회사 코드 필터
|
||||
if (companyCode) {
|
||||
query += ` AND company_name = $${paramIndex}`;
|
||||
queryParams.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (status) {
|
||||
query += ` AND status = $${paramIndex}`;
|
||||
queryParams.push(status);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 검색 조건
|
||||
if (search) {
|
||||
query += ` AND (
|
||||
dept_name ILIKE $${paramIndex} OR
|
||||
dept_code ILIKE $${paramIndex} OR
|
||||
location_name ILIKE $${paramIndex}
|
||||
)`;
|
||||
queryParams.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 정렬 (상위 부서 먼저, 그 다음 부서명 순)
|
||||
query += ` ORDER BY
|
||||
CASE WHEN parent_dept_code IS NULL OR parent_dept_code = '' THEN 0 ELSE 1 END,
|
||||
dept_name`;
|
||||
|
||||
const result = await client.query(query, queryParams);
|
||||
const departments = result.rows;
|
||||
|
||||
// 부서 트리 구조 생성
|
||||
const deptMap = new Map();
|
||||
const rootDepartments: any[] = [];
|
||||
|
||||
// 모든 부서를 맵에 저장
|
||||
departments.forEach((dept) => {
|
||||
deptMap.set(dept.dept_code, {
|
||||
deptCode: dept.dept_code,
|
||||
deptName: dept.dept_name,
|
||||
parentDeptCode: dept.parent_dept_code,
|
||||
masterSabun: dept.master_sabun,
|
||||
masterUserId: dept.master_user_id,
|
||||
location: dept.location,
|
||||
locationName: dept.location_name,
|
||||
regdate: dept.regdate ? dept.regdate.toISOString() : null,
|
||||
dataType: dept.data_type,
|
||||
status: dept.status || "active",
|
||||
salesYn: dept.sales_yn,
|
||||
companyName: dept.company_name,
|
||||
children: [],
|
||||
});
|
||||
});
|
||||
|
||||
// 부서 트리 구조 생성
|
||||
departments.forEach((dept) => {
|
||||
const deptNode = deptMap.get(dept.dept_code);
|
||||
|
||||
if (dept.parent_dept_code && deptMap.has(dept.parent_dept_code)) {
|
||||
// 상위 부서가 있으면 children에 추가
|
||||
const parentDept = deptMap.get(dept.parent_dept_code);
|
||||
parentDept.children.push(deptNode);
|
||||
} else {
|
||||
// 상위 부서가 없으면 루트 부서로 추가
|
||||
rootDepartments.push(deptNode);
|
||||
}
|
||||
});
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
data: {
|
||||
departments: rootDepartments,
|
||||
flatList: departments.map((dept) => ({
|
||||
deptCode: dept.dept_code,
|
||||
deptName: dept.dept_name,
|
||||
parentDeptCode: dept.parent_dept_code,
|
||||
masterSabun: dept.master_sabun,
|
||||
masterUserId: dept.master_user_id,
|
||||
location: dept.location,
|
||||
locationName: dept.location_name,
|
||||
regdate: dept.regdate ? dept.regdate.toISOString() : null,
|
||||
dataType: dept.data_type,
|
||||
status: dept.status || "active",
|
||||
salesYn: dept.sales_yn,
|
||||
companyName: dept.company_name,
|
||||
})),
|
||||
},
|
||||
message: "부서 목록 조회 성공",
|
||||
total: departments.length,
|
||||
};
|
||||
|
||||
logger.info("부서 목록 조회 성공", {
|
||||
totalCount: departments.length,
|
||||
rootCount: rootDepartments.length,
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("부서 목록 조회 실패", { error });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "부서 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* GET /api/admin/users/:userId
|
||||
* 사용자 상세 조회 API
|
||||
* 기존 Java AdminController의 사용자 상세 조회 기능 포팅
|
||||
*/
|
||||
export const getUserInfo = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
logger.info(`사용자 상세 조회 요청 - userId: ${userId}`, {
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "사용자 ID가 필요합니다.",
|
||||
error: {
|
||||
code: "USER_ID_REQUIRED",
|
||||
details: "userId parameter is required",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// PostgreSQL 클라이언트 생성
|
||||
const client = new Client({
|
||||
connectionString:
|
||||
process.env.DATABASE_URL ||
|
||||
"postgresql://postgres:postgres@localhost:5432/ilshin",
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
// 사용자 상세 정보 조회 쿼리
|
||||
const query = `
|
||||
SELECT
|
||||
u.sabun,
|
||||
u.user_id,
|
||||
u.user_name,
|
||||
u.user_name_eng,
|
||||
u.user_name_cn,
|
||||
u.dept_code,
|
||||
u.dept_name,
|
||||
u.position_code,
|
||||
u.position_name,
|
||||
u.email,
|
||||
u.tel,
|
||||
u.cell_phone,
|
||||
u.user_type,
|
||||
u.user_type_name,
|
||||
u.regdate,
|
||||
u.status,
|
||||
u.end_date,
|
||||
u.fax_no,
|
||||
u.partner_objid,
|
||||
u.rank,
|
||||
u.locale,
|
||||
u.company_code,
|
||||
u.data_type,
|
||||
d.dept_name as dept_name_full,
|
||||
d.parent_dept_code,
|
||||
d.location,
|
||||
d.location_name,
|
||||
d.sales_yn,
|
||||
d.company_name as dept_company_name
|
||||
FROM user_info u
|
||||
LEFT JOIN dept_info d ON u.dept_code = d.dept_code
|
||||
WHERE u.user_id = $1
|
||||
`;
|
||||
|
||||
const result = await client.query(query, [userId]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "사용자를 찾을 수 없습니다.",
|
||||
error: {
|
||||
code: "USER_NOT_FOUND",
|
||||
details: `User ID: ${userId}`,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
|
||||
// 응답 데이터 가공
|
||||
const userInfo = {
|
||||
sabun: user.sabun,
|
||||
userId: user.user_id,
|
||||
userName: user.user_name,
|
||||
userNameEng: user.user_name_eng,
|
||||
userNameCn: user.user_name_cn,
|
||||
deptCode: user.dept_code,
|
||||
deptName: user.dept_name || user.dept_name_full,
|
||||
positionCode: user.position_code,
|
||||
positionName: user.position_name,
|
||||
email: user.email,
|
||||
tel: user.tel,
|
||||
cellPhone: user.cell_phone,
|
||||
userType: user.user_type,
|
||||
userTypeName: user.user_type_name,
|
||||
regdate: user.regdate ? user.regdate.toISOString() : null,
|
||||
status: user.status || "active",
|
||||
endDate: user.end_date ? user.end_date.toISOString() : null,
|
||||
faxNo: user.fax_no,
|
||||
partnerObjid: user.partner_objid,
|
||||
rank: user.rank,
|
||||
locale: user.locale,
|
||||
companyCode: user.company_code,
|
||||
dataType: user.data_type,
|
||||
// 부서 정보
|
||||
deptInfo: {
|
||||
deptCode: user.dept_code,
|
||||
deptName: user.dept_name || user.dept_name_full,
|
||||
parentDeptCode: user.parent_dept_code,
|
||||
location: user.location,
|
||||
locationName: user.location_name,
|
||||
salesYn: user.sales_yn,
|
||||
companyName: user.dept_company_name,
|
||||
},
|
||||
};
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
data: userInfo,
|
||||
message: "사용자 상세 정보 조회 성공",
|
||||
};
|
||||
|
||||
logger.info("사용자 상세 정보 조회 성공", {
|
||||
userId,
|
||||
userName: user.user_name,
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("사용자 상세 정보 조회 실패", {
|
||||
error,
|
||||
userId: req.params.userId,
|
||||
});
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "사용자 상세 정보 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* POST /api/admin/users/check-duplicate
|
||||
* 사용자 ID 중복 체크 API
|
||||
* 기존 Java AdminController의 checkDuplicateUserId 기능 포팅
|
||||
*/
|
||||
export const checkDuplicateUserId = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { userId } = req.body;
|
||||
logger.info(`사용자 ID 중복 체크 요청 - userId: ${userId}`, {
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "사용자 ID가 필요합니다.",
|
||||
error: {
|
||||
code: "USER_ID_REQUIRED",
|
||||
details: "userId is required",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// PostgreSQL 클라이언트 생성
|
||||
const client = new Client({
|
||||
connectionString:
|
||||
process.env.DATABASE_URL ||
|
||||
"postgresql://postgres:postgres@localhost:5432/ilshin",
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
// 사용자 ID 중복 체크 쿼리
|
||||
const query = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM user_info
|
||||
WHERE user_id = $1
|
||||
`;
|
||||
|
||||
const result = await client.query(query, [userId]);
|
||||
const count = parseInt(result.rows[0].count);
|
||||
|
||||
const isDuplicate = count > 0;
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
data: {
|
||||
isDuplicate,
|
||||
count,
|
||||
message: isDuplicate
|
||||
? "이미 사용 중인 사용자 ID입니다."
|
||||
: "사용 가능한 사용자 ID입니다.",
|
||||
},
|
||||
message: "사용자 ID 중복 체크 완료",
|
||||
};
|
||||
|
||||
logger.info("사용자 ID 중복 체크 완료", {
|
||||
userId,
|
||||
isDuplicate,
|
||||
count,
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("사용자 ID 중복 체크 실패", {
|
||||
error,
|
||||
userId: req.body?.userId,
|
||||
});
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "사용자 ID 중복 체크 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* POST /api/admin/users
|
||||
* 사용자 등록/수정 API
|
||||
* 기존 Java AdminController의 saveUserInfo 기능 포팅
|
||||
*/
|
||||
export const saveUser = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const userData = req.body;
|
||||
logger.info("사용자 저장 요청", { userData, user: req.user });
|
||||
|
||||
// 필수 필드 검증
|
||||
const requiredFields = ["userId", "userName", "userPassword"];
|
||||
for (const field of requiredFields) {
|
||||
if (!userData[field] || userData[field].trim() === "") {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: `${field}는 필수 입력 항목입니다.`,
|
||||
error: {
|
||||
code: "REQUIRED_FIELD_MISSING",
|
||||
details: `Required field: ${field}`,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// PostgreSQL 클라이언트 생성
|
||||
const client = new Client({
|
||||
connectionString:
|
||||
process.env.DATABASE_URL ||
|
||||
"postgresql://postgres:postgres@localhost:5432/ilshin",
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
// 기존 사용자 확인
|
||||
const checkQuery = `
|
||||
SELECT user_id FROM user_info WHERE user_id = $1
|
||||
`;
|
||||
const checkResult = await client.query(checkQuery, [userData.userId]);
|
||||
const isUpdate = checkResult.rows.length > 0;
|
||||
|
||||
if (isUpdate) {
|
||||
// 기존 사용자 수정
|
||||
const updateQuery = `
|
||||
UPDATE user_info SET
|
||||
user_name = $1,
|
||||
user_name_eng = $2,
|
||||
user_password = $3,
|
||||
dept_code = $4,
|
||||
dept_name = $5,
|
||||
position_code = $6,
|
||||
position_name = $7,
|
||||
email = $8,
|
||||
tel = $9,
|
||||
cell_phone = $10,
|
||||
user_type = $11,
|
||||
user_type_name = $12,
|
||||
sabun = $13,
|
||||
company_code = $14,
|
||||
status = $15,
|
||||
locale = $16
|
||||
WHERE user_id = $17
|
||||
`;
|
||||
|
||||
const updateValues = [
|
||||
userData.userName,
|
||||
userData.userNameEng || null,
|
||||
await EncryptUtil.encrypt(userData.userPassword), // 비밀번호 암호화
|
||||
userData.deptCode || null,
|
||||
userData.deptName || null,
|
||||
userData.positionCode || null,
|
||||
userData.positionName || null,
|
||||
userData.email || null,
|
||||
userData.tel || null,
|
||||
userData.cellPhone || null,
|
||||
userData.userType || null,
|
||||
userData.userTypeName || null,
|
||||
userData.sabun || null,
|
||||
userData.companyCode || null,
|
||||
userData.status || "active",
|
||||
userData.locale || null,
|
||||
userData.userId,
|
||||
];
|
||||
|
||||
await client.query(updateQuery, updateValues);
|
||||
logger.info("사용자 정보 수정 완료", { userId: userData.userId });
|
||||
} else {
|
||||
// 새 사용자 등록
|
||||
const insertQuery = `
|
||||
INSERT INTO user_info (
|
||||
user_id, user_name, user_name_eng, user_password, dept_code, dept_name,
|
||||
position_code, position_name, email, tel, cell_phone, user_type,
|
||||
user_type_name, sabun, company_code, status, locale, regdate
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18
|
||||
)
|
||||
`;
|
||||
|
||||
const insertValues = [
|
||||
userData.userId,
|
||||
userData.userName,
|
||||
userData.userNameEng || null,
|
||||
await EncryptUtil.encrypt(userData.userPassword), // 비밀번호 암호화
|
||||
userData.deptCode || null,
|
||||
userData.deptName || null,
|
||||
userData.positionCode || null,
|
||||
userData.positionName || null,
|
||||
userData.email || null,
|
||||
userData.tel || null,
|
||||
userData.cellPhone || null,
|
||||
userData.userType || null,
|
||||
userData.userTypeName || null,
|
||||
userData.sabun || null,
|
||||
userData.companyCode || null,
|
||||
userData.status || "active",
|
||||
userData.locale || null,
|
||||
new Date(),
|
||||
];
|
||||
|
||||
await client.query(insertQuery, insertValues);
|
||||
logger.info("새 사용자 등록 완료", { userId: userData.userId });
|
||||
}
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
result: true,
|
||||
message: isUpdate
|
||||
? "사용자 정보가 수정되었습니다."
|
||||
: "사용자가 등록되었습니다.",
|
||||
data: {
|
||||
userId: userData.userId,
|
||||
isUpdate,
|
||||
},
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("사용자 저장 실패", { error, userData: req.body });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
result: false,
|
||||
message: "사용자 저장 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ import {
|
|||
deleteMenu, // 메뉴 삭제
|
||||
deleteMenusBatch, // 메뉴 일괄 삭제
|
||||
getUserList,
|
||||
getUserInfo, // 사용자 상세 조회
|
||||
getDepartmentList, // 부서 목록 조회
|
||||
checkDuplicateUserId, // 사용자 ID 중복 체크
|
||||
saveUser, // 사용자 등록/수정
|
||||
getCompanyList,
|
||||
getCompanyListFromDB, // 실제 DB에서 회사 목록 조회
|
||||
getUserLocale,
|
||||
|
|
@ -42,6 +46,12 @@ router.delete("/menus/:menuId", deleteMenu); // 메뉴 삭제
|
|||
|
||||
// 사용자 관리 API
|
||||
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
|
||||
router.get("/companies", getCompanyList);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
// 인증 서비스
|
||||
// 기존 Java LoginService를 Node.js로 포팅
|
||||
|
||||
import prisma from "../config/database";
|
||||
import { PasswordUtils } from "../utils/passwordUtils";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { JwtUtils } from "../utils/jwtUtils";
|
||||
import { EncryptUtil } from "../utils/encryptUtil";
|
||||
import { PersonBean, LoginResult, LoginLogData } from "../types/auth";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export class AuthService {
|
||||
/**
|
||||
* 기존 Java LoginService.loginPwdCheck() 메서드 포팅
|
||||
|
|
@ -42,7 +44,7 @@ export class AuthService {
|
|||
}
|
||||
|
||||
// 비밀번호 검증 (기존 EncryptUtil 로직 사용)
|
||||
if (PasswordUtils.matches(password, dbPassword)) {
|
||||
if (EncryptUtil.matches(password, dbPassword)) {
|
||||
logger.info(`비밀번호 일치로 로그인 성공: ${userId}`);
|
||||
return {
|
||||
loginResult: true,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -763,6 +763,195 @@ export const logger = winston.createLogger({
|
|||
- [ ] 사용자 생성/수정 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부터 실제 데이터베이스 연동으로 구현
|
||||
|
||||
#### **Phase 2-2A: 메뉴 관리 API (완료 ✅)**
|
||||
|
||||
- [x] 관리자 메뉴 조회 API (`GET /api/admin/menus`) - **완료: 기존 `AdminController.getAdminMenuList()` 포팅**
|
||||
|
|
|
|||
|
|
@ -62,10 +62,24 @@ interface CompanyOption {
|
|||
}
|
||||
|
||||
interface DepartmentOption {
|
||||
CODE: string;
|
||||
NAME: string;
|
||||
DEPT_CODE: string;
|
||||
DEPT_NAME: string;
|
||||
deptCode?: string;
|
||||
deptName?: string;
|
||||
parentDeptCode?: 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; // 기타 필드들
|
||||
}
|
||||
|
||||
|
|
@ -202,20 +216,21 @@ export function UserFormModal({ isOpen, onClose, onSuccess }: UserFormModalProps
|
|||
try {
|
||||
const response = await userAPI.checkDuplicateId(formData.userId);
|
||||
if (response.success && response.data) {
|
||||
// result는 boolean 타입: true = 사용가능, false = 중복됨
|
||||
const isAvailable = response.data.result;
|
||||
// 백엔드 API 응답 구조: { isDuplicate: boolean, message: string }
|
||||
const isDuplicate = response.data.isDuplicate;
|
||||
const message = response.data.message;
|
||||
|
||||
if (isAvailable) {
|
||||
// 중복체크 성공 시 상태 업데이트
|
||||
if (!isDuplicate) {
|
||||
// 중복되지 않음 (사용 가능)
|
||||
setIsUserIdChecked(true);
|
||||
setLastCheckedUserId(formData.userId);
|
||||
setDuplicateCheckMessage(response.data.msg || "사용 가능한 사용자 ID입니다.");
|
||||
setDuplicateCheckMessage(message || "사용 가능한 사용자 ID입니다.");
|
||||
setDuplicateCheckType("success");
|
||||
} else {
|
||||
// 중복된 ID인 경우 상태 초기화
|
||||
// 중복됨 (사용 불가)
|
||||
setIsUserIdChecked(false);
|
||||
setLastCheckedUserId("");
|
||||
setDuplicateCheckMessage(response.data.msg || "이미 사용 중인 사용자 ID입니다.");
|
||||
setDuplicateCheckMessage(message || "이미 사용 중인 사용자 ID입니다.");
|
||||
setDuplicateCheckType("error");
|
||||
}
|
||||
}
|
||||
|
|
@ -280,15 +295,15 @@ export function UserFormModal({ isOpen, onClose, onSuccess }: UserFormModalProps
|
|||
|
||||
try {
|
||||
const userDataToSend = {
|
||||
user_id: formData.userId,
|
||||
user_password: formData.userPassword,
|
||||
user_name: formData.userName,
|
||||
userId: formData.userId,
|
||||
userPassword: formData.userPassword,
|
||||
userName: formData.userName,
|
||||
email: formData.email || null,
|
||||
tel: formData.tel || null,
|
||||
cell_phone: formData.cellPhone || null,
|
||||
position_name: formData.positionName || null,
|
||||
company_code: formData.companyCode,
|
||||
dept_code: formData.deptCode || null,
|
||||
cellPhone: formData.cellPhone || null,
|
||||
positionName: formData.positionName || null,
|
||||
companyCode: formData.companyCode,
|
||||
deptCode: formData.deptCode || null,
|
||||
sabun: null, // 항상 null (테이블 1번 컬럼)
|
||||
status: "active", // 기본값 (테이블 18번 컬럼)
|
||||
};
|
||||
|
|
@ -460,14 +475,28 @@ export function UserFormModal({ isOpen, onClose, onSuccess }: UserFormModalProps
|
|||
<SelectValue placeholder="부서 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{departments.map((department) => (
|
||||
<SelectItem
|
||||
key={department.CODE || department.DEPT_CODE}
|
||||
value={department.CODE || department.DEPT_CODE}
|
||||
>
|
||||
{department.NAME || department.DEPT_NAME}
|
||||
{Array.isArray(departments) && departments.length > 0 ? (
|
||||
departments
|
||||
.filter((department) => {
|
||||
const deptCode = department.deptCode || department.CODE || department.DEPT_CODE;
|
||||
return deptCode && deptCode.trim() !== "";
|
||||
})
|
||||
.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>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -180,23 +180,23 @@ export function UserTable({ users, isLoading, paginationInfo, onStatusToggle, on
|
|||
{users.map((user, index) => (
|
||||
<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">{user.sabun}</TableCell>
|
||||
<TableCell className="font-medium">{user.company_name || "-"}</TableCell>
|
||||
<TableCell className="font-medium">{user.dept_name}</TableCell>
|
||||
<TableCell className="font-medium">{user.position_name || "-"}</TableCell>
|
||||
<TableCell className="font-mono">{user.user_id}</TableCell>
|
||||
<TableCell className="font-medium">{user.user_name}</TableCell>
|
||||
<TableCell>{user.tel || user.cell_phone || "-"}</TableCell>
|
||||
<TableCell className="font-mono text-sm">{user.sabun || "-"}</TableCell>
|
||||
<TableCell className="font-medium">{user.companyCode || "-"}</TableCell>
|
||||
<TableCell className="font-medium">{user.deptName || "-"}</TableCell>
|
||||
<TableCell className="font-medium">{user.positionName || "-"}</TableCell>
|
||||
<TableCell className="font-mono">{user.userId}</TableCell>
|
||||
<TableCell className="font-medium">{user.userName}</TableCell>
|
||||
<TableCell>{user.tel || user.cellPhone || "-"}</TableCell>
|
||||
<TableCell className="max-w-[200px] truncate" title={user.email}>
|
||||
{user.email || "-"}
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(user.regdate)}</TableCell>
|
||||
<TableCell>{formatDate(user.regDate)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={user.status === "active"}
|
||||
onCheckedChange={(checked) => handleStatusToggle(user, checked)}
|
||||
aria-label={`${user.user_name} 상태 토글`}
|
||||
aria-label={`${user.userName} 상태 토글`}
|
||||
/>
|
||||
<span
|
||||
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
|
||||
variant="ghost"
|
||||
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"
|
||||
title="비밀번호 초기화"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -144,7 +144,9 @@ export async function getDepartmentList(companyCode?: string) {
|
|||
const response = await apiClient.get(`/admin/departments${params}`);
|
||||
|
||||
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 || "부서 목록 조회에 실패했습니다.");
|
||||
|
|
|
|||
|
|
@ -2,43 +2,35 @@
|
|||
* 사용자 관리 관련 타입 정의
|
||||
*/
|
||||
|
||||
// 사용자 정보 인터페이스 (프론트엔드 친화적 camelCase)
|
||||
// 사용자 정보 인터페이스 (백엔드 API 응답과 일치하는 camelCase)
|
||||
export interface User {
|
||||
sabun: string; // 사번
|
||||
user_id: string; // 사용자 ID
|
||||
user_name: string; // 사용자명
|
||||
user_name_eng?: string; // 영문명
|
||||
user_name_cn?: string; // 중문명
|
||||
company_name?: string; // 회사명
|
||||
dept_code: string; // 부서 코드
|
||||
dept_name: string; // 부서명
|
||||
position_code?: string; // 직책 코드
|
||||
position_name: string; // 직책
|
||||
email: string; // 이메일
|
||||
tel: string; // 전화번호
|
||||
cell_phone: string; // 휴대폰
|
||||
user_type?: string; // 사용자 유형 코드
|
||||
user_type_name: string; // 사용자 유형명
|
||||
regdate: string; // 등록일 (YYYY-MM-DD)
|
||||
regdate_org?: string; // 원본 등록일
|
||||
sabun?: string; // 사번
|
||||
userId: string; // 사용자 ID
|
||||
userName: string; // 사용자명
|
||||
userNameEng?: string; // 영문명
|
||||
userNameCn?: string; // 중문명
|
||||
companyCode?: string; // 회사 코드
|
||||
companyName?: string; // 회사명
|
||||
deptCode?: string; // 부서 코드
|
||||
deptName?: string; // 부서명
|
||||
positionCode?: string; // 직책 코드
|
||||
positionName?: string; // 직책
|
||||
email?: string; // 이메일
|
||||
tel?: string; // 전화번호
|
||||
cellPhone?: string; // 휴대폰
|
||||
userType?: string; // 사용자 유형 코드
|
||||
userTypeName?: string; // 사용자 유형명
|
||||
regDate?: string; // 등록일 (YYYY-MM-DD)
|
||||
status: string; // 상태 (active, inactive)
|
||||
data_type?: string; // 데이터 타입
|
||||
enddate?: string; // 퇴사일
|
||||
dataType?: string; // 데이터 타입
|
||||
endDate?: string; // 퇴사일
|
||||
locale?: string; // 로케일
|
||||
rnum?: number; // 행 번호
|
||||
}
|
||||
|
||||
// 사용자 검색 필터
|
||||
export interface UserSearchFilter {
|
||||
searchType?:
|
||||
| "all"
|
||||
| "sabun"
|
||||
| "company_name"
|
||||
| "dept_name"
|
||||
| "position_name"
|
||||
| "user_id"
|
||||
| "user_name"
|
||||
| "tel"
|
||||
| "email"; // 검색 대상
|
||||
searchType?: "all" | "sabun" | "companyCode" | "deptName" | "positionName" | "userId" | "userName" | "tel" | "email"; // 검색 대상
|
||||
searchValue?: string; // 검색어
|
||||
}
|
||||
|
||||
|
|
@ -52,15 +44,15 @@ export interface UserTableColumn {
|
|||
|
||||
// 사용자 등록/수정 폼 데이터
|
||||
export interface UserFormData {
|
||||
user_id: string;
|
||||
user_name: string;
|
||||
dept_code: string;
|
||||
dept_name: string;
|
||||
position_name: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
deptCode: string;
|
||||
deptName: string;
|
||||
positionName: string;
|
||||
email: string;
|
||||
tel: string;
|
||||
cell_phone: string;
|
||||
user_type_name: string;
|
||||
cellPhone: string;
|
||||
userTypeName: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue