최초커밋

This commit is contained in:
kjs
2025-08-21 09:41:46 +09:00
commit a0e5b57a24
2454 changed files with 1476904 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
// JWT 토큰 관리 유틸리티
// 기존 PersonBean 정보를 JWT 페이로드로 변환
import jwt from "jsonwebtoken";
import { PersonBean, JwtPayload } from "../types/auth";
import config from "../config/environment";
export class JwtUtils {
/**
* 사용자 정보로 JWT 토큰 생성
* 기존 PersonBean 정보를 JWT 페이로드로 변환
*/
static generateToken(userInfo: PersonBean): string {
try {
const payload: JwtPayload = {
userId: userInfo.userId,
userName: userInfo.userName,
deptName: userInfo.deptName,
companyCode: userInfo.companyCode,
userType: userInfo.userType,
userTypeName: userInfo.userTypeName,
};
return jwt.sign(payload, config.jwt.secret, {
expiresIn: config.jwt.expiresIn,
issuer: "PMS-System",
audience: "PMS-Users",
} as any);
} catch (error) {
console.error("JWT token generation error:", error);
throw new Error("토큰 생성 중 오류가 발생했습니다.");
}
}
/**
* JWT 토큰 검증 및 사용자 정보 추출
*/
static verifyToken(token: string): PersonBean {
try {
const decoded = jwt.verify(token, config.jwt.secret) as JwtPayload;
// PersonBean 형태로 변환
const personBean: PersonBean = {
userId: decoded.userId,
userName: decoded.userName,
deptName: decoded.deptName,
companyCode: decoded.companyCode,
userType: decoded.userType,
userTypeName: decoded.userTypeName,
};
return personBean;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new Error("토큰이 만료되었습니다.");
} else if (error instanceof jwt.JsonWebTokenError) {
throw new Error("유효하지 않은 토큰입니다.");
} else {
console.error("JWT token verification error:", error);
throw new Error("토큰 검증 중 오류가 발생했습니다.");
}
}
}
/**
* JWT 토큰에서 페이로드만 추출 (검증 없이)
*/
static decodeToken(token: string): JwtPayload | null {
try {
return jwt.decode(token) as JwtPayload;
} catch (error) {
console.error("JWT token decode error:", error);
return null;
}
}
/**
* 토큰 만료 시간 확인
*/
static isTokenExpired(token: string): boolean {
try {
const decoded = jwt.decode(token) as JwtPayload;
if (!decoded || !decoded.exp) {
return true;
}
const currentTime = Math.floor(Date.now() / 1000);
return decoded.exp < currentTime;
} catch (error) {
console.error("Token expiration check error:", error);
return true;
}
}
/**
* 토큰 갱신 (만료 시간만 연장)
*/
static refreshToken(token: string): string {
try {
const decoded = jwt.decode(token) as JwtPayload;
if (!decoded) {
throw new Error("토큰을 디코드할 수 없습니다.");
}
// 페이로드에서 만료 시간 관련 필드 제거
const { iat, exp, aud, iss, ...payload } = decoded;
return jwt.sign(payload, config.jwt.secret, {
expiresIn: config.jwt.expiresIn,
issuer: "PMS-System",
} as any);
} catch (error) {
console.error("JWT token refresh error:", error);
throw new Error("토큰 갱신 중 오류가 발생했습니다.");
}
}
/**
* 토큰에서 사용자 ID 추출
*/
static getUserIdFromToken(token: string): string | null {
try {
const decoded = jwt.decode(token) as JwtPayload;
return decoded?.userId || null;
} catch (error) {
console.error("Get user ID from token error:", error);
return null;
}
}
/**
* 토큰 유효성 검사 (만료 여부 포함)
*/
static validateToken(token: string): { isValid: boolean; error?: string } {
try {
jwt.verify(token, config.jwt.secret);
return { isValid: true };
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return { isValid: false, error: "토큰이 만료되었습니다." };
} else if (error instanceof jwt.JsonWebTokenError) {
return { isValid: false, error: "유효하지 않은 토큰입니다." };
} else {
return { isValid: false, error: "토큰 검증 중 오류가 발생했습니다." };
}
}
}
/**
* 테스트용 메서드 (개발 환경에서만 사용)
*/
static testJwtUtils(userInfo: PersonBean): void {
console.log("=== JWT 토큰 테스트 ===");
console.log("사용자 정보:", userInfo);
const token = this.generateToken(userInfo);
console.log("생성된 토큰:", token);
const decoded = this.decodeToken(token);
console.log("디코드된 페이로드:", decoded);
const verified = this.verifyToken(token);
console.log("검증된 사용자 정보:", verified);
const isExpired = this.isTokenExpired(token);
console.log("토큰 만료 여부:", isExpired);
const userId = this.getUserIdFromToken(token);
console.log("토큰에서 추출한 사용자 ID:", userId);
const validation = this.validateToken(token);
console.log("토큰 유효성 검사:", validation);
}
}

View File

@@ -0,0 +1,66 @@
import winston from "winston";
import config from "../config/environment";
// 로그 포맷 정의
const logFormat = winston.format.combine(
winston.format.timestamp({
format: "YYYY-MM-DD HH:mm:ss",
}),
winston.format.errors({ stack: true }),
winston.format.json()
);
// 콘솔 포맷 (개발 환경용)
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({
format: "YYYY-MM-DD HH:mm:ss",
}),
winston.format.printf(({ timestamp, level, message, stack }) => {
if (stack) {
return `${timestamp} [${level}]: ${message}\n${stack}`;
}
return `${timestamp} [${level}]: ${message}`;
})
);
// 로거 생성
export const logger = winston.createLogger({
level: config.logging.level,
format: logFormat,
transports: [
// 파일 로그 (에러)
new winston.transports.File({
filename: "logs/error.log",
level: "error",
maxsize: 5242880, // 5MB
maxFiles: 5,
}),
// 파일 로그 (전체)
new winston.transports.File({
filename: "logs/combined.log",
maxsize: 5242880, // 5MB
maxFiles: 5,
}),
],
});
// 개발 환경에서는 콘솔 출력 추가
if (config.nodeEnv !== "production") {
logger.add(
new winston.transports.Console({
format: consoleFormat,
})
);
}
// 로그 디렉토리 생성
import fs from "fs";
import path from "path";
const logsDir = path.join(process.cwd(), "logs");
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
export default logger;

View File

@@ -0,0 +1,197 @@
// 기존 Java EncryptUtil 클래스를 Node.js로 포팅
// AES/ECB/NoPadding 암호화 방식 사용
import crypto from "crypto";
// 기존 Java Constants에서 가져온 값들
const KEY_NAME = "ILJIAESSECRETKEY";
const ALGORITHM = "AES";
const MASTER_PWD = "qlalfqjsgh11";
export class PasswordUtils {
/**
* 기존 Java EncryptUtil.encrypt() 메서드 포팅
* AES/ECB/NoPadding 방식으로 암호화
*/
static encrypt(source: string): string {
try {
// 16바이트 키 생성 (AES-128)
const key = Buffer.from(KEY_NAME, "utf8").slice(0, 16);
// 패딩 추가 (16바이트 블록 크기에 맞춤)
const paddedData = this.addPadding(Buffer.from(source, "utf8"));
// AES 암호화
const cipher = crypto.createCipher("aes-128-ecb", key);
cipher.setAutoPadding(false); // NoPadding 모드
let encrypted = cipher.update(paddedData);
encrypted = Buffer.concat([encrypted, cipher.final()]);
// 16진수 문자열로 변환
return this.fromHex(encrypted);
} catch (error) {
console.error("Password encryption error:", error);
throw new Error("암호화 중 오류가 발생했습니다.");
}
}
/**
* 기존 Java EncryptUtil.decrypt() 메서드 포팅
*/
static decrypt(source: string): string {
try {
// 16바이트 키 생성
const key = Buffer.from(KEY_NAME, "utf8").slice(0, 16);
// 16진수 문자열을 바이트 배열로 변환
const encryptedData = this.toBytes(source);
// AES 복호화
const decipher = crypto.createDecipher("aes-128-ecb", key);
decipher.setAutoPadding(false); // NoPadding 모드
let decrypted = decipher.update(encryptedData);
decrypted = Buffer.concat([decrypted, decipher.final()]);
// 패딩 제거
const unpaddedData = this.removePadding(decrypted);
return unpaddedData.toString("utf8");
} catch (error) {
console.error("Password decryption error:", error);
throw new Error("복호화 중 오류가 발생했습니다.");
}
}
/**
* 기존 Java EncryptUtil.encryptSha256() 메서드 포팅
*/
static encryptSha256(s: string): string {
try {
const hash = crypto.createHash("sha256");
hash.update(s, "utf8");
return hash.digest("hex");
} catch (error) {
console.error("SHA256 encryption error:", error);
return "";
}
}
/**
* 비밀번호 검증 (기존 Java 로직과 동일)
*/
static matches(plainPassword: string, encryptedPassword: string): boolean {
try {
// 마스터 패스워드 체크
if (MASTER_PWD === plainPassword) {
return true;
}
// 일반 패스워드 암호화 후 비교
const encryptedPlainPassword = this.encrypt(plainPassword);
return encryptedPlainPassword === encryptedPassword;
} catch (error) {
console.error("Password matching error:", error);
return false;
}
}
/**
* 기존 Java addPadding() 메서드 포팅
* 16바이트 블록 크기에 맞춰 패딩 추가
*/
private static addPadding(pBytes: Buffer): Buffer {
const pCount = pBytes.length;
const tCount = pCount + (16 - (pCount % 16));
const tBytes = Buffer.alloc(tCount);
pBytes.copy(tBytes, 0);
// 나머지 바이트를 0x00으로 채움
for (let rIndex = pCount; rIndex < tCount; rIndex++) {
tBytes[rIndex] = 0x00;
}
return tBytes;
}
/**
* 기존 Java removePadding() 메서드 포팅
* 패딩 제거
*/
private static removePadding(pBytes: Buffer): Buffer {
const pCount = pBytes.length;
let index = 0;
let loop = true;
while (loop) {
if (index === pCount || pBytes[index] === 0x00) {
loop = false;
index--;
}
index++;
}
const tBytes = Buffer.alloc(index);
pBytes.copy(tBytes, 0, 0, index);
return tBytes;
}
/**
* 기존 Java toBytes() 메서드 포팅
* 16진수 문자열을 바이트 배열로 변환
*/
private static toBytes(pSource: string): Buffer {
const buff = pSource;
const bCount = Math.floor(buff.length / 2);
const bArr = Buffer.alloc(bCount);
for (let bIndex = 0; bIndex < bCount; bIndex++) {
const hexByte = buff.substring(2 * bIndex, 2 * bIndex + 2);
bArr[bIndex] = parseInt(hexByte, 16);
}
return bArr;
}
/**
* 기존 Java fromHex() 메서드 포팅
* 바이트 배열을 16진수 문자열로 변환
*/
private static fromHex(pBytes: Buffer): string {
const pCount = pBytes.length;
let buff = "";
for (let pIndex = 0; pIndex < pCount; pIndex++) {
const byte = pBytes[pIndex] & 0xff;
if (byte < 0x10) {
buff += "0";
}
buff += byte.toString(16);
}
return buff;
}
/**
* 테스트용 메서드 (개발 환경에서만 사용)
*/
static testEncryption(plainText: string): void {
console.log("=== 암호화 테스트 ===");
console.log("원본 텍스트:", plainText);
const encrypted = this.encrypt(plainText);
console.log("암호화 결과:", encrypted);
const decrypted = this.decrypt(encrypted);
console.log("복호화 결과:", decrypted);
const isMatch = this.matches(plainText, encrypted);
console.log("비밀번호 일치:", isMatch);
const sha256Hash = this.encryptSha256(plainText);
console.log("SHA256 해시:", sha256Hash);
}
}