2025-08-21 09:41:46 +09:00
|
|
|
// 인증 컨트롤러
|
|
|
|
|
// 기존 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";
|
2026-03-11 18:34:58 +09:00
|
|
|
import { sendSmartFactoryLog } from "../utils/smartFactoryLog";
|
2026-04-07 10:46:54 +09:00
|
|
|
import { query, queryOne } from "../database/db";
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
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";
|
|
|
|
|
|
2026-02-12 11:42:52 +09:00
|
|
|
logger.debug(`로그인 요청: ${userId}`);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
// 입력값 검증
|
|
|
|
|
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",
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-12 11:42:52 +09:00
|
|
|
logger.debug(`로그인 사용자 정보: ${userInfo.userId} (${userInfo.companyCode})`);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
2026-03-09 15:15:15 +09:00
|
|
|
// 메뉴 조회를 위한 공통 파라미터
|
|
|
|
|
const { AdminService } = await import("../services/adminService");
|
|
|
|
|
const paramMap = {
|
|
|
|
|
userId: loginResult.userInfo.userId,
|
|
|
|
|
userCompanyCode: loginResult.userInfo.companyCode || "ILSHIN",
|
|
|
|
|
userType: loginResult.userInfo.userType,
|
|
|
|
|
userLang: "ko",
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-28 14:55:41 +09:00
|
|
|
// 사용자의 첫 번째 접근 가능한 메뉴 조회
|
|
|
|
|
let firstMenuPath: string | null = null;
|
2026-03-29 20:04:52 +09:00
|
|
|
let firstMenuName: string | null = null;
|
2025-10-28 14:55:41 +09:00
|
|
|
try {
|
|
|
|
|
const menuList = await AdminService.getUserMenuList(paramMap);
|
2026-02-12 11:42:52 +09:00
|
|
|
logger.debug(`로그인 후 메뉴 조회: 총 ${menuList.length}개 메뉴`);
|
2025-10-28 14:55:41 +09:00
|
|
|
|
|
|
|
|
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;
|
2026-03-29 20:04:52 +09:00
|
|
|
firstMenuName = firstMenu.menu_name_kor || firstMenu.translated_name || firstMenu.menu_name || null;
|
|
|
|
|
logger.debug(`첫 번째 메뉴: ${firstMenuPath} (${firstMenuName})`);
|
2025-10-28 14:55:41 +09:00
|
|
|
} else {
|
2026-02-12 11:42:52 +09:00
|
|
|
logger.debug("접근 가능한 메뉴 없음, 메인 페이지로 이동");
|
2025-10-28 14:55:41 +09:00
|
|
|
}
|
|
|
|
|
} catch (menuError) {
|
|
|
|
|
logger.warn("메뉴 조회 중 오류 발생 (무시하고 계속):", menuError);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 18:34:58 +09:00
|
|
|
// 스마트공장 활용 로그 전송 (비동기, 응답 블로킹 안 함)
|
|
|
|
|
sendSmartFactoryLog({
|
|
|
|
|
userId: userInfo.userId,
|
2026-04-07 10:35:16 +09:00
|
|
|
userName: userInfo.userName,
|
2026-03-11 18:34:58 +09:00
|
|
|
remoteAddr,
|
|
|
|
|
useType: "접속",
|
2026-04-03 11:23:02 +09:00
|
|
|
companyCode: userInfo.companyCode,
|
2026-03-11 18:34:58 +09:00
|
|
|
}).catch(() => {});
|
|
|
|
|
|
2026-03-09 15:15:15 +09:00
|
|
|
// 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}`);
|
2026-04-07 10:46:54 +09:00
|
|
|
|
|
|
|
|
// POP 메뉴가 존재하면 해당 회사의 POP 레이아웃 자동 초기화 (비동기)
|
|
|
|
|
if (popLandingPath) {
|
|
|
|
|
const companyCode = loginResult.userInfo.companyCode || "ILSHIN";
|
|
|
|
|
AuthController.initPopLayoutsForCompany(companyCode).catch((err) => {
|
|
|
|
|
logger.warn("POP 레이아웃 자동 초기화 중 오류 (무시):", err);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-09 15:15:15 +09:00
|
|
|
} catch (popError) {
|
|
|
|
|
logger.warn("POP 메뉴 조회 중 오류 (무시):", popError);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
res.status(200).json({
|
|
|
|
|
success: true,
|
|
|
|
|
message: "로그인 성공",
|
|
|
|
|
data: {
|
|
|
|
|
userInfo,
|
|
|
|
|
token: loginResult.token,
|
2026-03-11 18:34:58 +09:00
|
|
|
firstMenuPath,
|
2026-03-29 20:04:52 +09:00
|
|
|
firstMenuName,
|
2026-03-09 15:15:15 +09:00
|
|
|
popLandingPath,
|
2025-08-21 09:41:46 +09:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} 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
|
|
|
|
|
: "알 수 없는 오류가 발생했습니다.",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 15:28:05 +09:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
: "알 수 없는 오류가 발생했습니다.",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
|
2025-08-28 10:05:06 +09:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-05 21:52:19 +09:00
|
|
|
// 프론트엔드 호환성을 위해 더 많은 사용자 정보 반환
|
2025-12-30 15:28:05 +09:00
|
|
|
// ⚠️ JWT 토큰의 companyCode를 우선 사용 (회사 전환 기능 지원)
|
2025-09-05 21:52:19 +09:00
|
|
|
const userInfoResponse: any = {
|
2025-08-28 10:05:06 +09:00
|
|
|
userId: dbUserInfo.userId,
|
|
|
|
|
userName: dbUserInfo.userName || "",
|
|
|
|
|
deptName: dbUserInfo.deptName || "",
|
2025-12-30 15:28:05 +09:00
|
|
|
companyCode: userInfo.companyCode || dbUserInfo.companyCode || "ILSHIN", // JWT 토큰 우선
|
|
|
|
|
company_code: userInfo.companyCode || dbUserInfo.companyCode || "ILSHIN", // JWT 토큰 우선
|
|
|
|
|
userType: userInfo.userType || dbUserInfo.userType || "USER", // JWT 토큰 우선
|
2025-08-28 10:05:06 +09:00
|
|
|
userTypeName: dbUserInfo.userTypeName || "일반사용자",
|
|
|
|
|
email: dbUserInfo.email || "",
|
|
|
|
|
photo: dbUserInfo.photo,
|
|
|
|
|
locale: dbUserInfo.locale || "KR", // locale 정보 추가
|
2025-09-05 21:52:19 +09:00
|
|
|
deptCode: dbUserInfo.deptCode, // 추가 필드
|
2025-08-21 09:41:46 +09:00
|
|
|
isAdmin:
|
2025-08-28 10:05:06 +09:00
|
|
|
dbUserInfo.userType === "ADMIN" || dbUserInfo.userId === "plm_admin",
|
2025-08-21 09:41:46 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2025-08-21 13:28:49 +09:00
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
res.status(200).json({
|
|
|
|
|
success: true,
|
|
|
|
|
message: "세션 상태 확인",
|
|
|
|
|
data: {
|
2025-08-21 13:28:49 +09:00
|
|
|
isLoggedIn: true,
|
|
|
|
|
isAdmin: isAdmin,
|
2025-08-21 09:41:46 +09:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} 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
|
|
|
|
|
: "알 수 없는 오류가 발생했습니다.",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-01 18:41:02 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 : "알 수 없는 오류",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-07 10:46:54 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* POP 레이아웃 자동 초기화
|
|
|
|
|
* 해당 회사의 screen_layouts_pop 레코드가 없으면
|
|
|
|
|
* 템플릿(공통 '*' 또는 COMPANY_7)에서 복제하여 생성
|
|
|
|
|
*
|
|
|
|
|
* 기본 POP 화면 ID: 5, 6, 7, 8, 6526, 6527, 6528, 6529
|
|
|
|
|
*/
|
|
|
|
|
static async initPopLayoutsForCompany(companyCode: string): Promise<void> {
|
|
|
|
|
// SUPER_ADMIN이나 공통(*)은 초기화 불필요
|
|
|
|
|
if (companyCode === "*" || companyCode === "COMPANY_7") return;
|
|
|
|
|
|
|
|
|
|
const POP_SCREEN_IDS = [5, 6, 7, 8, 6526, 6527, 6528, 6529];
|
|
|
|
|
|
|
|
|
|
// 이미 해당 회사의 POP 레이아웃이 하나라도 있으면 스킵 (중복 초기화 방지)
|
|
|
|
|
const existing = await query<{ cnt: string }>(
|
|
|
|
|
`SELECT COUNT(*)::text AS cnt FROM screen_layouts_pop
|
|
|
|
|
WHERE company_code = $1 AND screen_id = ANY($2::int[])`,
|
|
|
|
|
[companyCode, POP_SCREEN_IDS],
|
|
|
|
|
);
|
|
|
|
|
const existingCount = parseInt(existing[0]?.cnt || "0", 10);
|
|
|
|
|
if (existingCount > 0) {
|
|
|
|
|
logger.debug(`POP 레이아웃 이미 존재 (${companyCode}): ${existingCount}개, 스킵`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info(`POP 레이아웃 자동 초기화 시작: ${companyCode}`);
|
|
|
|
|
|
|
|
|
|
// 회사명 조회 (레이아웃 내 회사명 치환용)
|
|
|
|
|
const companyInfo = await queryOne<{ company_name: string }>(
|
|
|
|
|
`SELECT company_name FROM company_mng WHERE company_code = $1`,
|
|
|
|
|
[companyCode],
|
|
|
|
|
);
|
|
|
|
|
const companyName = companyInfo?.company_name || companyCode;
|
|
|
|
|
|
|
|
|
|
let initCount = 0;
|
|
|
|
|
for (const screenId of POP_SCREEN_IDS) {
|
|
|
|
|
// 템플릿 조회: 공통(*) 우선, 없으면 COMPANY_7 폴백
|
|
|
|
|
let template = await queryOne<{ layout_data: any }>(
|
|
|
|
|
`SELECT layout_data FROM screen_layouts_pop
|
|
|
|
|
WHERE screen_id = $1 AND company_code = '*'`,
|
|
|
|
|
[screenId],
|
|
|
|
|
);
|
|
|
|
|
if (!template) {
|
|
|
|
|
template = await queryOne<{ layout_data: any }>(
|
|
|
|
|
`SELECT layout_data FROM screen_layouts_pop
|
|
|
|
|
WHERE screen_id = $1 AND company_code = 'COMPANY_7'`,
|
|
|
|
|
[screenId],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!template) {
|
|
|
|
|
logger.debug(`POP 템플릿 없음 (screen_id=${screenId}), 스킵`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 레이아웃 복제 + 회사명 치환
|
|
|
|
|
const layoutStr = JSON.stringify(template.layout_data);
|
|
|
|
|
const replacedStr = layoutStr
|
|
|
|
|
.replace(/\(주\)탑씰/g, companyName)
|
|
|
|
|
.replace(/탑씰/g, companyName)
|
|
|
|
|
.replace(/TOPSEAL/gi, companyName);
|
|
|
|
|
|
|
|
|
|
await query(
|
|
|
|
|
`INSERT INTO screen_layouts_pop (screen_id, company_code, layout_data, created_at, updated_at, created_by, updated_by)
|
|
|
|
|
VALUES ($1, $2, $3, NOW(), NOW(), 'SYSTEM', 'SYSTEM')
|
|
|
|
|
ON CONFLICT (screen_id, company_code) DO NOTHING`,
|
|
|
|
|
[screenId, companyCode, replacedStr],
|
|
|
|
|
);
|
|
|
|
|
initCount++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info(`POP 레이아웃 자동 초기화 완료: ${companyCode}, ${initCount}개 화면`);
|
|
|
|
|
}
|
2025-08-21 09:41:46 +09:00
|
|
|
}
|