Files
vexplor_dev/backend-node/src/controllers/authController.ts
kjs ca7b145c7f feat: Enhance logging and configuration for Smart Factory integration
- Added companyCode to the AuthController for improved logging.
- Updated sendSmartFactoryLog function to support company-specific API keys.
- Configured environment variables for company-specific Smart Factory API keys in Docker Compose files.

This update ensures that logs are more informative and that the correct API keys are used based on the company context.
2026-04-03 11:23:02 +09:00

566 lines
18 KiB
TypeScript

// 인증 컨트롤러
// 기존 Java ApiLoginController를 Node.js로 포팅
import { Request, Response } from "express";
import { AuthService } from "../services/authService";
import { JwtUtils } from "../utils/jwtUtils";
import { LoginRequest, UserInfo, ApiResponse, PersonBean } from "../types/auth";
import { logger } from "../utils/logger";
import { sendSmartFactoryLog } from "../utils/smartFactoryLog";
export class AuthController {
/**
* POST /api/auth/login
* 기존 Java ApiLoginController.login() 메서드 포팅
*/
static async login(req: Request, res: Response): Promise<void> {
try {
const { userId, password }: LoginRequest = req.body;
const remoteAddr = req.ip || req.connection.remoteAddress || "unknown";
logger.debug(`로그인 요청: ${userId}`);
// 입력값 검증
if (!userId || !password) {
res.status(400).json({
success: false,
message: "사용자 ID와 비밀번호를 입력해주세요.",
error: {
code: "INVALID_INPUT",
details: "필수 입력값이 누락되었습니다.",
},
});
return;
}
// 로그인 프로세스 실행
const loginResult = await AuthService.processLogin(
userId,
password,
remoteAddr
);
if (loginResult.success && loginResult.userInfo && loginResult.token) {
// 로그인 성공
const userInfo: UserInfo = {
userId: loginResult.userInfo.userId,
userName: loginResult.userInfo.userName || "",
deptName: loginResult.userInfo.deptName || "",
companyCode: loginResult.userInfo.companyCode || "ILSHIN",
};
logger.debug(`로그인 사용자 정보: ${userInfo.userId} (${userInfo.companyCode})`);
// 메뉴 조회를 위한 공통 파라미터
const { AdminService } = await import("../services/adminService");
const paramMap = {
userId: loginResult.userInfo.userId,
userCompanyCode: loginResult.userInfo.companyCode || "ILSHIN",
userType: loginResult.userInfo.userType,
userLang: "ko",
};
// 사용자의 첫 번째 접근 가능한 메뉴 조회
let firstMenuPath: string | null = null;
let firstMenuName: string | null = null;
try {
const menuList = await AdminService.getUserMenuList(paramMap);
logger.debug(`로그인 후 메뉴 조회: 총 ${menuList.length}개 메뉴`);
const firstMenu = menuList.find((menu: any) => {
const level = menu.lev || menu.level;
const url = menu.menu_url || menu.url;
return level >= 2 && url && url.trim() !== "" && url !== "#";
});
if (firstMenu) {
firstMenuPath = firstMenu.menu_url || firstMenu.url;
firstMenuName = firstMenu.menu_name_kor || firstMenu.translated_name || firstMenu.menu_name || null;
logger.debug(`첫 번째 메뉴: ${firstMenuPath} (${firstMenuName})`);
} else {
logger.debug("접근 가능한 메뉴 없음, 메인 페이지로 이동");
}
} catch (menuError) {
logger.warn("메뉴 조회 중 오류 발생 (무시하고 계속):", menuError);
}
// 스마트공장 활용 로그 전송 (비동기, 응답 블로킹 안 함)
sendSmartFactoryLog({
userId: userInfo.userId,
remoteAddr,
useType: "접속",
companyCode: userInfo.companyCode,
}).catch(() => {});
// POP 랜딩 경로 조회
let popLandingPath: string | null = null;
try {
const popResult = await AdminService.getPopMenuList(paramMap);
if (popResult.landingMenu?.menu_url) {
popLandingPath = popResult.landingMenu.menu_url;
} else if (popResult.childMenus.length === 1) {
popLandingPath = popResult.childMenus[0].menu_url;
} else if (popResult.childMenus.length > 1) {
popLandingPath = "/pop";
}
logger.debug(`POP 랜딩 경로: ${popLandingPath}`);
} catch (popError) {
logger.warn("POP 메뉴 조회 중 오류 (무시):", popError);
}
res.status(200).json({
success: true,
message: "로그인 성공",
data: {
userInfo,
token: loginResult.token,
firstMenuPath,
firstMenuName,
popLandingPath,
},
});
} else {
// 로그인 실패
res.status(401).json({
success: false,
message: "로그인 실패",
error: {
code: "LOGIN_FAILED",
details:
loginResult.errorReason || "알 수 없는 오류가 발생했습니다.",
},
});
}
} catch (error) {
logger.error(
`로그인 API 오류: ${error instanceof Error ? error.message : error}`
);
res.status(500).json({
success: false,
message: "서버 오류가 발생했습니다.",
error: {
code: "SERVER_ERROR",
details:
error instanceof Error
? error.message
: "알 수 없는 오류가 발생했습니다.",
},
});
}
}
/**
* POST /api/auth/switch-company
* WACE 관리자 전용: 다른 회사로 전환
*/
static async switchCompany(req: Request, res: Response): Promise<void> {
try {
const { companyCode } = req.body;
const authHeader = req.get("Authorization");
const token = authHeader && authHeader.split(" ")[1];
if (!token) {
res.status(401).json({
success: false,
message: "인증 토큰이 필요합니다.",
error: { code: "TOKEN_MISSING" },
});
return;
}
// 현재 사용자 정보 확인
const currentUser = JwtUtils.verifyToken(token);
// WACE 관리자 권한 체크 (userType = "SUPER_ADMIN"만 확인)
// 이미 다른 회사로 전환한 상태(companyCode != "*")에서도 다시 전환 가능해야 함
if (currentUser.userType !== "SUPER_ADMIN") {
logger.warn(`회사 전환 권한 없음: userId=${currentUser.userId}, userType=${currentUser.userType}, companyCode=${currentUser.companyCode}`);
res.status(403).json({
success: false,
message: "회사 전환은 최고 관리자(SUPER_ADMIN)만 가능합니다.",
error: { code: "FORBIDDEN" },
});
return;
}
// 전환할 회사 코드 검증
if (!companyCode || companyCode.trim() === "") {
res.status(400).json({
success: false,
message: "전환할 회사 코드가 필요합니다.",
error: { code: "INVALID_INPUT" },
});
return;
}
logger.info(`=== WACE 관리자 회사 전환 ===`, {
userId: currentUser.userId,
originalCompanyCode: currentUser.companyCode,
targetCompanyCode: companyCode,
});
// 회사 코드 존재 여부 확인 (company_code가 "*"가 아닌 경우만)
if (companyCode !== "*") {
const { query } = await import("../database/db");
const companies = await query<any>(
"SELECT company_code, company_name FROM company_mng WHERE company_code = $1",
[companyCode]
);
if (companies.length === 0) {
res.status(404).json({
success: false,
message: "존재하지 않는 회사 코드입니다.",
error: { code: "COMPANY_NOT_FOUND" },
});
return;
}
}
// 새로운 JWT 토큰 발급 (company_code만 변경)
const newPersonBean: PersonBean = {
...currentUser,
companyCode: companyCode.trim(), // 전환할 회사 코드로 변경
};
const newToken = JwtUtils.generateToken(newPersonBean);
logger.info(`✅ 회사 전환 성공: ${currentUser.userId}${companyCode}`);
res.status(200).json({
success: true,
message: "회사 전환 완료",
data: {
token: newToken,
companyCode: companyCode.trim(),
},
});
} catch (error) {
logger.error(
`회사 전환 API 오류: ${error instanceof Error ? error.message : error}`
);
res.status(500).json({
success: false,
message: "회사 전환 중 오류가 발생했습니다.",
error: {
code: "SERVER_ERROR",
details:
error instanceof Error
? error.message
: "알 수 없는 오류가 발생했습니다.",
},
});
}
}
/**
* POST /api/auth/logout
* 기존 Java ApiLoginController.logout() 메서드 포팅
*/
static async logout(req: Request, res: Response): Promise<void> {
try {
const remoteAddr = req.ip || req.connection.remoteAddress || "unknown";
// JWT 토큰에서 사용자 정보 추출
const authHeader = req.get("Authorization");
const token = authHeader && authHeader.split(" ")[1];
if (token) {
try {
const userInfo = JwtUtils.verifyToken(token);
await AuthService.processLogout(userInfo.userId, remoteAddr);
} catch (tokenError) {
logger.warn(
`로그아웃 시 토큰 검증 실패: ${tokenError instanceof Error ? tokenError.message : tokenError}`
);
}
}
res.status(200).json({
success: true,
message: "로그아웃되었습니다.",
data: null,
});
} catch (error) {
logger.error(
`로그아웃 API 오류: ${error instanceof Error ? error.message : error}`
);
res.status(500).json({
success: false,
message: "로그아웃 처리 중 오류가 발생했습니다.",
error: {
code: "LOGOUT_ERROR",
details:
error instanceof Error
? error.message
: "알 수 없는 오류가 발생했습니다.",
},
});
}
}
/**
* GET /api/auth/me
* 기존 Java ApiLoginController.getCurrentUser() 메서드 포팅
*/
static async getCurrentUser(req: Request, res: Response): Promise<void> {
try {
const authHeader = req.get("Authorization");
const token = authHeader && authHeader.split(" ")[1];
if (!token) {
res.status(401).json({
success: false,
message: "인증되지 않은 사용자입니다.",
error: {
code: "NOT_AUTHENTICATED",
details: "세션이 만료되었거나 로그인이 필요합니다.",
},
});
return;
}
const userInfo = JwtUtils.verifyToken(token);
// DB에서 최신 사용자 정보 조회 (locale 포함)
const dbUserInfo = await AuthService.getUserInfo(userInfo.userId);
if (!dbUserInfo) {
res.status(401).json({
success: false,
message: "사용자 정보를 찾을 수 없습니다.",
error: {
code: "USER_NOT_FOUND",
details: "사용자 정보가 삭제되었거나 존재하지 않습니다.",
},
});
return;
}
// 프론트엔드 호환성을 위해 더 많은 사용자 정보 반환
// ⚠️ JWT 토큰의 companyCode를 우선 사용 (회사 전환 기능 지원)
const userInfoResponse: any = {
userId: dbUserInfo.userId,
userName: dbUserInfo.userName || "",
deptName: dbUserInfo.deptName || "",
companyCode: userInfo.companyCode || dbUserInfo.companyCode || "ILSHIN", // JWT 토큰 우선
company_code: userInfo.companyCode || dbUserInfo.companyCode || "ILSHIN", // JWT 토큰 우선
userType: userInfo.userType || dbUserInfo.userType || "USER", // JWT 토큰 우선
userTypeName: dbUserInfo.userTypeName || "일반사용자",
email: dbUserInfo.email || "",
photo: dbUserInfo.photo,
locale: dbUserInfo.locale || "KR", // locale 정보 추가
deptCode: dbUserInfo.deptCode, // 추가 필드
isAdmin:
dbUserInfo.userType === "ADMIN" || dbUserInfo.userId === "plm_admin",
};
res.status(200).json({
success: true,
message: "사용자 정보 조회 성공",
data: userInfoResponse,
});
} catch (error) {
logger.error(
`사용자 정보 조회 API 오류: ${error instanceof Error ? error.message : error}`
);
res.status(401).json({
success: false,
message: "인증되지 않은 사용자입니다.",
error: {
code: "NOT_AUTHENTICATED",
details: "세션이 만료되었거나 로그인이 필요합니다.",
},
});
}
}
/**
* GET /api/auth/status
* 기존 Java ApiLoginController.checkAuthStatus() 메서드 포팅
*/
static async checkAuthStatus(req: Request, res: Response): Promise<void> {
try {
const authHeader = req.get("Authorization");
const token = authHeader && authHeader.split(" ")[1];
if (!token) {
res.status(200).json({
success: true,
message: "세션 상태 확인",
data: {
isLoggedIn: false,
isAdmin: false,
},
});
return;
}
const validation = JwtUtils.validateToken(token);
if (!validation.isValid) {
res.status(200).json({
success: true,
message: "세션 상태 확인",
data: {
isLoggedIn: false,
isAdmin: false,
error: validation.error,
},
});
return;
}
// 토큰에서 사용자 정보 추출하여 관리자 권한 확인
let isAdmin = false;
try {
const userInfo = JwtUtils.verifyToken(token);
// 기존 Java 로직과 동일: plm_admin 사용자만 관리자로 인식
isAdmin =
userInfo.userId === "plm_admin" || userInfo.userType === "ADMIN";
logger.info(`인증 상태 확인: ${userInfo.userId}, 관리자: ${isAdmin}`);
} catch (error) {
logger.error(`토큰에서 사용자 정보 추출 실패: ${error}`);
}
res.status(200).json({
success: true,
message: "세션 상태 확인",
data: {
isLoggedIn: true,
isAdmin: isAdmin,
},
});
} catch (error) {
logger.error(
`세션 상태 확인 API 오류: ${error instanceof Error ? error.message : error}`
);
res.status(500).json({
success: false,
message: "세션 상태 확인 중 오류가 발생했습니다.",
error: {
code: "SESSION_CHECK_ERROR",
details:
error instanceof Error
? error.message
: "알 수 없는 오류가 발생했습니다.",
},
});
}
}
/**
* POST /api/auth/refresh
* JWT 토큰 갱신 API
*/
static async refreshToken(req: Request, res: Response): Promise<void> {
try {
const authHeader = req.get("Authorization");
const token = authHeader && authHeader.split(" ")[1];
if (!token) {
res.status(401).json({
success: false,
message: "토큰이 필요합니다.",
error: {
code: "TOKEN_MISSING",
details: "인증 토큰이 필요합니다.",
},
});
return;
}
const newToken = JwtUtils.refreshToken(token);
res.status(200).json({
success: true,
message: "토큰 갱신 성공",
data: {
token: newToken,
},
});
} catch (error) {
logger.error(
`토큰 갱신 API 오류: ${error instanceof Error ? error.message : error}`
);
res.status(401).json({
success: false,
message: "토큰 갱신에 실패했습니다.",
error: {
code: "TOKEN_REFRESH_ERROR",
details:
error instanceof Error
? error.message
: "알 수 없는 오류가 발생했습니다.",
},
});
}
}
/**
* POST /api/auth/signup
* 공차중계 회원가입 API
*/
static async signup(req: Request, res: Response): Promise<void> {
try {
const { userId, password, userName, phoneNumber, licenseNumber, vehicleNumber, vehicleType } = req.body;
logger.info(`=== 공차중계 회원가입 API 호출 ===`);
logger.info(`userId: ${userId}, vehicleNumber: ${vehicleNumber}`);
// 입력값 검증
if (!userId || !password || !userName || !phoneNumber || !licenseNumber || !vehicleNumber) {
res.status(400).json({
success: false,
message: "필수 입력값이 누락되었습니다.",
error: {
code: "INVALID_INPUT",
details: "아이디, 비밀번호, 이름, 연락처, 면허번호, 차량번호는 필수입니다.",
},
});
return;
}
// 회원가입 처리
const signupResult = await AuthService.signupDriver({
userId,
password,
userName,
phoneNumber,
licenseNumber,
vehicleNumber,
vehicleType,
});
if (signupResult.success) {
logger.info(`공차중계 회원가입 성공: ${userId}`);
res.status(201).json({
success: true,
message: "회원가입이 완료되었습니다.",
});
} else {
logger.warn(`공차중계 회원가입 실패: ${userId} - ${signupResult.message}`);
res.status(400).json({
success: false,
message: signupResult.message || "회원가입에 실패했습니다.",
error: {
code: "SIGNUP_FAILED",
details: signupResult.message,
},
});
}
} catch (error) {
logger.error("공차중계 회원가입 API 오류:", error);
res.status(500).json({
success: false,
message: "회원가입 처리 중 오류가 발생했습니다.",
error: {
code: "SIGNUP_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
});
}
}
}