diff --git a/PHASE1.5_AUTH_MIGRATION_PLAN.md b/PHASE1.5_AUTH_MIGRATION_PLAN.md new file mode 100644 index 00000000..6b91ed50 --- /dev/null +++ b/PHASE1.5_AUTH_MIGRATION_PLAN.md @@ -0,0 +1,733 @@ +# ๐Ÿ” Phase 1.5: ์ธ์ฆ ๋ฐ ๊ด€๋ฆฌ์ž ์„œ๋น„์Šค Raw Query ์ „ํ™˜ ๊ณ„ํš + +## ๐Ÿ“‹ ๊ฐœ์š” + +Phase 2์˜ ํ•ต์‹ฌ ์„œ๋น„์Šค ์ „ํ™˜ ์ „์— **์ธ์ฆ ๋ฐ ๊ด€๋ฆฌ์ž ์‹œ์Šคํ…œ**์„ ๋จผ์ € Raw Query๋กœ ์ „ํ™˜ํ•˜์—ฌ ์ „์ฒด ์‹œ์Šคํ…œ์˜ ์•ˆ์ •์ ์ธ ๊ธฐ๋ฐ˜์„ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. + +### ๐ŸŽฏ ๋ชฉํ‘œ + +- AuthService์˜ 5๊ฐœ Prisma ํ˜ธ์ถœ ์ œ๊ฑฐ +- AdminService์˜ 3๊ฐœ Prisma ํ˜ธ์ถœ ์ œ๊ฑฐ (์ด๋ฏธ Raw Query ์‚ฌ์šฉ ์ค‘) +- AdminController์˜ 28๊ฐœ Prisma ํ˜ธ์ถœ ์ œ๊ฑฐ +- ๋กœ๊ทธ์ธ โ†’ ์ธ์ฆ โ†’ API ํ˜ธ์ถœ ์ „์ฒด ํ”Œ๋กœ์šฐ ๊ฒ€์ฆ + +### ๐Ÿ“Š ์ „ํ™˜ ๋Œ€์ƒ + +| ์„œ๋น„์Šค | Prisma ํ˜ธ์ถœ ์ˆ˜ | ๋ณต์žก๋„ | ์šฐ์„ ์ˆœ์œ„ | +|--------|----------------|--------|----------| +| AuthService | 5๊ฐœ | ์ค‘๊ฐ„ | ๐Ÿ”ด ์ตœ์šฐ์„  | +| AdminService | 3๊ฐœ | ๋‚ฎ์Œ (์ด๋ฏธ Raw Query) | ๐ŸŸข ํ™•์ธ๋งŒ ํ•„์š” | +| AdminController | 28๊ฐœ | ์ค‘๊ฐ„ | ๐ŸŸก 2์ˆœ์œ„ | + +--- + +## ๐Ÿ” AuthService ๋ถ„์„ + +### Prisma ์‚ฌ์šฉ ํ˜„ํ™ฉ (5๊ฐœ) + +```typescript +// Line 21: loginPwdCheck() - ์‚ฌ์šฉ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ ์กฐํšŒ +const userInfo = await prisma.user_info.findUnique({ + where: { user_id: userId }, + select: { user_password: true }, +}); + +// Line 82: insertLoginAccessLog() - ๋กœ๊ทธ์ธ ๋กœ๊ทธ ๊ธฐ๋ก +await prisma.$executeRaw`INSERT INTO LOGIN_ACCESS_LOG(...)`; + +// Line 126: getUserInfo() - ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ +const userInfo = await prisma.user_info.findUnique({ + where: { user_id: userId }, + select: { /* 20๊ฐœ ํ•„๋“œ */ }, +}); + +// Line 157: getUserInfo() - ๊ถŒํ•œ ์ •๋ณด ์กฐํšŒ +const authInfo = await prisma.authority_sub_user.findMany({ + where: { user_id: userId }, + include: { authority_master: { select: { auth_name: true } } }, +}); + +// Line 177: getUserInfo() - ํšŒ์‚ฌ ์ •๋ณด ์กฐํšŒ +const companyInfo = await prisma.company_mng.findFirst({ + where: { company_code: userInfo.company_code || "ILSHIN" }, + select: { company_name: true }, +}); +``` + +### ํ•ต์‹ฌ ๋ฉ”์„œ๋“œ + +1. **loginPwdCheck()** - ๋กœ๊ทธ์ธ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ + - user_info ํ…Œ์ด๋ธ” ์กฐํšŒ + - ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ๋น„๊ต + - ๋งˆ์Šคํ„ฐ ํŒจ์Šค์›Œ๋“œ ์ฒดํฌ + +2. **insertLoginAccessLog()** - ๋กœ๊ทธ์ธ ์ด๋ ฅ ๊ธฐ๋ก + - LOGIN_ACCESS_LOG ํ…Œ์ด๋ธ” INSERT + - Raw Query ์ด๋ฏธ ์‚ฌ์šฉ ์ค‘ (์œ ์ง€) + +3. **getUserInfo()** - ์‚ฌ์šฉ์ž ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ + - user_info ํ…Œ์ด๋ธ” ์กฐํšŒ (20๊ฐœ ํ•„๋“œ) + - authority_sub_user + authority_master ์กฐ์ธ (๊ถŒํ•œ) + - company_mng ํ…Œ์ด๋ธ” ์กฐํšŒ (ํšŒ์‚ฌ๋ช…) + - PersonBean ํƒ€์ž… ๋ณ€ํ™˜ + +4. **processLogin()** - ๋กœ๊ทธ์ธ ์ „์ฒด ํ”„๋กœ์„ธ์Šค + - ์œ„ 3๊ฐœ ๋ฉ”์„œ๋“œ ์กฐํ•ฉ + - JWT ํ† ํฐ ์ƒ์„ฑ + +--- + +## ๐Ÿ› ๏ธ ์ „ํ™˜ ๊ณ„ํš + +### Step 1: loginPwdCheck() ์ „ํ™˜ + +**๊ธฐ์กด Prisma ์ฝ”๋“œ:** +```typescript +const userInfo = await prisma.user_info.findUnique({ + where: { user_id: userId }, + select: { user_password: true }, +}); +``` + +**์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ:** +```typescript +import { query } from "../database/db"; + +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; +``` + +### Step 2: getUserInfo() ์ „ํ™˜ (์‚ฌ์šฉ์ž ์ •๋ณด) + +**๊ธฐ์กด Prisma ์ฝ”๋“œ:** +```typescript +const userInfo = await prisma.user_info.findUnique({ + where: { user_id: userId }, + select: { + sabun: true, + user_id: true, + user_name: true, + // ... 20๊ฐœ ํ•„๋“œ + }, +}); +``` + +**์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ:** +```typescript +const result = 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 = result.length > 0 ? result[0] : null; +``` + +### Step 3: getUserInfo() ์ „ํ™˜ (๊ถŒํ•œ ์ •๋ณด) + +**๊ธฐ์กด Prisma ์ฝ”๋“œ:** +```typescript +const authInfo = await prisma.authority_sub_user.findMany({ + where: { user_id: userId }, + include: { + authority_master: { + select: { auth_name: true }, + }, + }, +}); + +const authNames = authInfo + .filter((auth: any) => auth.authority_master?.auth_name) + .map((auth: any) => auth.authority_master!.auth_name!) + .join(","); +``` + +**์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ:** +```typescript +const authResult = await query<{ auth_name: string }>( + `SELECT am.auth_name + FROM authority_sub_user asu + INNER JOIN authority_master am ON asu.auth_code = am.auth_code + WHERE asu.user_id = $1`, + [userId] +); + +const authNames = authResult.map(row => row.auth_name).join(","); +``` + +### Step 4: getUserInfo() ์ „ํ™˜ (ํšŒ์‚ฌ ์ •๋ณด) + +**๊ธฐ์กด Prisma ์ฝ”๋“œ:** +```typescript +const companyInfo = await prisma.company_mng.findFirst({ + where: { company_code: userInfo.company_code || "ILSHIN" }, + select: { company_name: true }, +}); +``` + +**์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ:** +```typescript +const companyResult = await query<{ company_name: string }>( + "SELECT company_name FROM company_mng WHERE company_code = $1", + [userInfo.company_code || "ILSHIN"] +); + +const companyInfo = companyResult.length > 0 ? companyResult[0] : null; +``` + +--- + +## ๐Ÿ“ ์™„์ „ ์ „ํ™˜๋œ AuthService ์ฝ”๋“œ + +```typescript +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 { + /** + * ๋กœ๊ทธ์ธ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ (Raw Query ์ „ํ™˜) + */ + static async loginPwdCheck( + userId: string, + password: string + ): Promise { + 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.info(`๋กœ๊ทธ์ธ ์‹œ๋„: ${userId}`); + logger.debug(`DB ๋น„๋ฐ€๋ฒˆํ˜ธ: ${dbPassword}, ์ž…๋ ฅ ๋น„๋ฐ€๋ฒˆํ˜ธ: ${password}`); + + // ๋งˆ์Šคํ„ฐ ํŒจ์Šค์›Œ๋“œ ์ฒดํฌ + if (password === "qlalfqjsgh11") { + logger.info(`๋งˆ์Šคํ„ฐ ํŒจ์Šค์›Œ๋“œ๋กœ ๋กœ๊ทธ์ธ ์„ฑ๊ณต: ${userId}`); + return { loginResult: true }; + } + + // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ + if (EncryptUtil.matches(password, dbPassword)) { + logger.info(`๋น„๋ฐ€๋ฒˆํ˜ธ ์ผ์น˜๋กœ ๋กœ๊ทธ์ธ ์„ฑ๊ณต: ${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: "๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }; + } + } + + /** + * ๋กœ๊ทธ์ธ ๋กœ๊ทธ ๊ธฐ๋ก (์ด๋ฏธ Raw Query ์‚ฌ์šฉ - ์œ ์ง€) + */ + static async insertLoginAccessLog(logData: LoginLogData): Promise { + try { + 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.info( + `๋กœ๊ทธ์ธ ๋กœ๊ทธ ๊ธฐ๋ก ์™„๋ฃŒ: ${logData.userId} (${logData.loginResult ? "์„ฑ๊ณต" : "์‹คํŒจ"})` + ); + } catch (error) { + logger.error( + `๋กœ๊ทธ์ธ ๋กœ๊ทธ ๊ธฐ๋ก ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error instanceof Error ? error.message : error}` + ); + // ๋กœ๊ทธ ๊ธฐ๋ก ์‹คํŒจ๋Š” ๋กœ๊ทธ์ธ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ค‘๋‹จํ•˜์ง€ ์•Š์Œ + } + } + + /** + * ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ (Raw Query ์ „ํ™˜) + */ + static async getUserInfo(userId: string): Promise { + try { + // 1. ์‚ฌ์šฉ์ž ๊ธฐ๋ณธ ์ •๋ณด ์กฐํšŒ + 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. ๊ถŒํ•œ ์ •๋ณด ์กฐํšŒ (JOIN์œผ๋กœ ์ตœ์ ํ™”) + const authResult = await query<{ auth_name: string }>( + `SELECT am.auth_name + FROM authority_sub_user asu + INNER JOIN authority_master am ON asu.auth_code = am.auth_code + WHERE asu.user_id = $1`, + [userId] + ); + + const authNames = authResult.map(row => row.auth_name).join(","); + + // 3. ํšŒ์‚ฌ ์ •๋ณด ์กฐํšŒ + const companyResult = await query<{ company_name: string }>( + "SELECT company_name FROM company_mng WHERE company_code = $1", + [userInfo.company_code || "ILSHIN"] + ); + + const companyInfo = companyResult.length > 0 ? companyResult[0] : null; + + // PersonBean ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ + 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: userInfo.user_type || undefined, + userTypeName: userInfo.user_type_name || undefined, + partnerObjid: userInfo.partner_objid || undefined, + authName: authNames || undefined, + companyCode: userInfo.company_code || "ILSHIN", + photo: userInfo.photo + ? `data:image/jpeg;base64,${Buffer.from(userInfo.photo).toString("base64")}` + : undefined, + locale: userInfo.locale || "KR", + }; + + logger.info(`์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ์™„๋ฃŒ: ${userId}`); + return personBean; + } catch (error) { + logger.error( + `์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error instanceof Error ? error.message : error}` + ); + return null; + } + } + + /** + * JWT ํ† ํฐ์œผ๋กœ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ + */ + static async getUserInfoFromToken(token: string): Promise { + 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 { + 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}` + ); + } + } +} +``` + +--- + +## ๐Ÿงช ํ…Œ์ŠคํŠธ ๊ณ„ํš + +### ๋‹จ์œ„ ํ…Œ์ŠคํŠธ + +```typescript +// backend-node/src/tests/authService.test.ts +import { AuthService } from "../services/authService"; +import { query } from "../database/db"; + +describe("AuthService Raw Query ์ „ํ™˜ ํ…Œ์ŠคํŠธ", () => { + describe("loginPwdCheck", () => { + test("์กด์žฌํ•˜๋Š” ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์„ฑ๊ณต", async () => { + const result = await AuthService.loginPwdCheck("testuser", "testpass"); + expect(result.loginResult).toBe(true); + }); + + test("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์‹คํŒจ", async () => { + const result = await AuthService.loginPwdCheck("nonexistent", "password"); + expect(result.loginResult).toBe(false); + expect(result.errorReason).toContain("์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"); + }); + + test("์ž˜๋ชป๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋กœ๊ทธ์ธ ์‹คํŒจ", async () => { + const result = await AuthService.loginPwdCheck("testuser", "wrongpass"); + expect(result.loginResult).toBe(false); + expect(result.errorReason).toContain("์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"); + }); + + test("๋งˆ์Šคํ„ฐ ํŒจ์Šค์›Œ๋“œ ๋กœ๊ทธ์ธ ์„ฑ๊ณต", async () => { + const result = await AuthService.loginPwdCheck("testuser", "qlalfqjsgh11"); + expect(result.loginResult).toBe(true); + }); + }); + + describe("getUserInfo", () => { + test("์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต", async () => { + const userInfo = await AuthService.getUserInfo("testuser"); + expect(userInfo).not.toBeNull(); + expect(userInfo?.userId).toBe("testuser"); + expect(userInfo?.userName).toBeDefined(); + }); + + test("๊ถŒํ•œ ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต", async () => { + const userInfo = await AuthService.getUserInfo("testuser"); + expect(userInfo?.authName).toBeDefined(); + }); + + test("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž ์กฐํšŒ ์‹คํŒจ", async () => { + const userInfo = await AuthService.getUserInfo("nonexistent"); + expect(userInfo).toBeNull(); + }); + }); + + describe("processLogin", () => { + test("์ „์ฒด ๋กœ๊ทธ์ธ ํ”„๋กœ์„ธ์Šค ์„ฑ๊ณต", async () => { + const result = await AuthService.processLogin( + "testuser", + "testpass", + "127.0.0.1" + ); + expect(result.success).toBe(true); + expect(result.token).toBeDefined(); + expect(result.userInfo).toBeDefined(); + }); + + test("๋กœ๊ทธ์ธ ์‹คํŒจ ์‹œ ํ† ํฐ ์—†์Œ", async () => { + const result = await AuthService.processLogin( + "testuser", + "wrongpass", + "127.0.0.1" + ); + expect(result.success).toBe(false); + expect(result.token).toBeUndefined(); + expect(result.errorReason).toBeDefined(); + }); + }); + + describe("insertLoginAccessLog", () => { + test("๋กœ๊ทธ์ธ ๋กœ๊ทธ ๊ธฐ๋ก ์„ฑ๊ณต", async () => { + await expect( + AuthService.insertLoginAccessLog({ + systemName: "PMS", + userId: "testuser", + loginResult: true, + remoteAddr: "127.0.0.1", + }) + ).resolves.not.toThrow(); + }); + }); +}); +``` + +### ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + +```typescript +// backend-node/src/tests/integration/auth.integration.test.ts +import request from "supertest"; +import app from "../../app"; + +describe("์ธ์ฆ ์‹œ์Šคํ…œ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ", () => { + let authToken: string; + + test("POST /api/auth/login - ๋กœ๊ทธ์ธ ์„ฑ๊ณต", async () => { + const response = await request(app) + .post("/api/auth/login") + .send({ + userId: "testuser", + password: "testpass", + }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.token).toBeDefined(); + expect(response.body.userInfo).toBeDefined(); + + authToken = response.body.token; + }); + + test("GET /api/auth/verify - ํ† ํฐ ๊ฒ€์ฆ ์„ฑ๊ณต", async () => { + const response = await request(app) + .get("/api/auth/verify") + .set("Authorization", `Bearer ${authToken}`) + .expect(200); + + expect(response.body.valid).toBe(true); + expect(response.body.userInfo).toBeDefined(); + }); + + test("GET /api/admin/menu - ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด ์กฐํšŒ", async () => { + const response = await request(app) + .get("/api/admin/menu") + .set("Authorization", `Bearer ${authToken}`) + .expect(200); + + expect(Array.isArray(response.body)).toBe(true); + }); + + test("POST /api/auth/logout - ๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต", async () => { + await request(app) + .post("/api/auth/logout") + .set("Authorization", `Bearer ${authToken}`) + .expect(200); + }); +}); +``` + +--- + +## ๐Ÿ“‹ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### AuthService ์ „ํ™˜ + +- [ ] import ๋ฌธ ๋ณ€๊ฒฝ (`prisma` โ†’ `query`) +- [ ] `loginPwdCheck()` ๋ฉ”์„œ๋“œ ์ „ํ™˜ + - [ ] Prisma findUnique โ†’ Raw Query SELECT + - [ ] ํƒ€์ž… ์ •์˜ ์ถ”๊ฐ€ + - [ ] ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ™•์ธ +- [ ] `insertLoginAccessLog()` ๋ฉ”์„œ๋“œ ํ™•์ธ + - [ ] ์ด๋ฏธ Raw Query ์‚ฌ์šฉ ์ค‘ (์œ ์ง€) + - [ ] ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ ํ™•์ธ +- [ ] `getUserInfo()` ๋ฉ”์„œ๋“œ ์ „ํ™˜ + - [ ] ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ Raw Query ์ „ํ™˜ + - [ ] ๊ถŒํ•œ ์ •๋ณด ์กฐํšŒ Raw Query ์ „ํ™˜ (JOIN ์ตœ์ ํ™”) + - [ ] ํšŒ์‚ฌ ์ •๋ณด ์กฐํšŒ Raw Query ์ „ํ™˜ + - [ ] PersonBean ํƒ€์ž… ๋ณ€ํ™˜ ๋กœ์ง ์œ ์ง€ +- [ ] ๋ชจ๋“  ๋ฉ”์„œ๋“œ ํƒ€์ž… ์•ˆ์ „์„ฑ ํ™•์ธ +- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ๋ฐ ํ†ต๊ณผ + +### AdminService ํ™•์ธ + +- [ ] ํ˜„์žฌ ์ฝ”๋“œ ํ™•์ธ (์ด๋ฏธ Raw Query ์‚ฌ์šฉ ์ค‘) +- [ ] WITH RECURSIVE ์ฟผ๋ฆฌ ๋™์ž‘ ํ™•์ธ +- [ ] ๋‹ค๊ตญ์–ด ๋ฒˆ์—ญ ๋กœ์ง ํ™•์ธ + +### AdminController ์ „ํ™˜ + +- [ ] Prisma ์‚ฌ์šฉ ํ˜„ํ™ฉ ํŒŒ์•… (28๊ฐœ ํ˜ธ์ถœ) +- [ ] ๊ฐ API ์—”๋“œํฌ์ธํŠธ๋ณ„ ์ „ํ™˜ ๊ณ„ํš ์ˆ˜๋ฆฝ +- [ ] Raw Query๋กœ ์ „ํ™˜ +- [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ + +### ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + +- [ ] ๋กœ๊ทธ์ธ โ†’ ํ† ํฐ ๋ฐœ๊ธ‰ ํ…Œ์ŠคํŠธ +- [ ] ํ† ํฐ ๊ฒ€์ฆ โ†’ API ํ˜ธ์ถœ ํ…Œ์ŠคํŠธ +- [ ] ๊ถŒํ•œ ํ™•์ธ โ†’ ๋ฉ”๋‰ด ์กฐํšŒ ํ…Œ์ŠคํŠธ +- [ ] ๋กœ๊ทธ์•„์›ƒ ํ…Œ์ŠคํŠธ +- [ ] ์—๋Ÿฌ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ + +--- + +## ๐ŸŽฏ ์™„๋ฃŒ ๊ธฐ์ค€ + +- โœ… AuthService์˜ ๋ชจ๋“  Prisma ํ˜ธ์ถœ ์ œ๊ฑฐ +- โœ… AdminService Raw Query ์‚ฌ์šฉ ํ™•์ธ +- โœ… AdminController Prisma ํ˜ธ์ถœ ์ œ๊ฑฐ +- โœ… ๋ชจ๋“  ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ +- โœ… ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ +- โœ… ๋กœ๊ทธ์ธ โ†’ ์ธ์ฆ โ†’ API ํ˜ธ์ถœ ํ”Œ๋กœ์šฐ ์ •์ƒ ๋™์ž‘ +- โœ… ์„ฑ๋Šฅ ์ €ํ•˜ ์—†์Œ (๊ธฐ์กด ๋Œ€๋น„ ยฑ10% ์ด๋‚ด) +- โœ… ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ๋กœ๊น… ์ •์ƒ ๋™์ž‘ + +--- + +## ๐Ÿ“š ์ฐธ๊ณ  ๋ฌธ์„œ + +- [Phase 1 ์™„๋ฃŒ ๊ฐ€์ด๋“œ](backend-node/PHASE1_USAGE_GUIDE.md) +- [DatabaseManager ์‚ฌ์šฉ๋ฒ•](backend-node/src/database/db.ts) +- [QueryBuilder ์‚ฌ์šฉ๋ฒ•](backend-node/src/utils/queryBuilder.ts) +- [์ „์ฒด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš](PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md) + +--- + +**์ž‘์„ฑ์ผ**: 2025-09-30 +**์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 2-3์ผ +**๋‹ด๋‹น์ž**: ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœํŒ€ \ No newline at end of file diff --git a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md index b6b0969c..4c2081c8 100644 --- a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md +++ b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md @@ -1040,6 +1040,27 @@ describe("Performance Benchmarks", () => { - [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (`backend-node/src/tests/`) - [x] ํ…Œ์ŠคํŠธ ์„ฑ๊ณต ํ™•์ธ (multiConnectionQueryService, externalCallConfigService) +### **Phase 1.5: ์ธ์ฆ ๋ฐ ๊ด€๋ฆฌ์ž ์„œ๋น„์Šค (์šฐ์„  ์ „ํ™˜) - 36๊ฐœ ํ˜ธ์ถœ** โšก **NEW** + +> **์šฐ์„ ์ˆœ์œ„ ๋ณ€๊ฒฝ**: Phase 2 ์ง„ํ–‰ ์ „ ์ธ์ฆ/๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ๋จผ์ € ์ „ํ™˜ํ•˜์—ฌ ์ „์ฒด ์‹œ์Šคํ…œ์˜ ์•ˆ์ •์ ์ธ ๊ธฐ๋ฐ˜ ๊ตฌ์ถ• + +- [ ] **AuthService ์ „ํ™˜ (5๊ฐœ)** - ๐Ÿ” ์ตœ์šฐ์„  + - [ ] ๋กœ๊ทธ์ธ ๋กœ์ง (JWT ์ƒ์„ฑ) + - [ ] ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๊ฒ€์ฆ + - [ ] ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ์ฒ˜๋ฆฌ + - [ ] ํ† ํฐ ๊ด€๋ฆฌ + - [ ] ์„ธ์…˜ ๊ด€๋ฆฌ +- [ ] **AdminService ์ „ํ™˜ (3๊ฐœ)** - ๐Ÿ‘ค ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ + - [ ] ์‚ฌ์šฉ์ž CRUD + - [ ] ๋ฉ”๋‰ด ๊ด€๋ฆฌ (์žฌ๊ท€ ์ฟผ๋ฆฌ) + - [ ] ๊ถŒํ•œ ๊ด€๋ฆฌ +- [ ] **AdminController ์ „ํ™˜ (28๊ฐœ)** - ๐Ÿ“ก ๊ด€๋ฆฌ์ž API + - [ ] ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ API + - [ ] ๋ฉ”๋‰ด ๊ด€๋ฆฌ API + - [ ] ๊ถŒํ•œ ๊ด€๋ฆฌ API + - [ ] ํšŒ์‚ฌ ๊ด€๋ฆฌ API +- [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (๋กœ๊ทธ์ธ โ†’ ์ธ์ฆ โ†’ API ํ˜ธ์ถœ ํ”Œ๋กœ์šฐ) + ### **Phase 2: ํ•ต์‹ฌ ์„œ๋น„์Šค (3์ฃผ) - 107๊ฐœ ํ˜ธ์ถœ** - [ ] ScreenManagementService ์ „ํ™˜ (46๊ฐœ) - ์ตœ์šฐ์„  @@ -1049,8 +1070,8 @@ describe("Performance Benchmarks", () => { - [ ] ExternalDbConnectionService ์ „ํ™˜ (15๊ฐœ) - [ ] DataflowControlService ์ „ํ™˜ (6๊ฐœ) - ๋ณต์žกํ•œ ๋กœ์ง - [ ] DDLExecutionService ์ „ํ™˜ (6๊ฐœ) -- [ ] AuthService ์ „ํ™˜ (5๊ฐœ) -- [ ] MultiConnectionQueryService ์ „ํ™˜ (4๊ฐœ) +- [ ] ~~AuthService ์ „ํ™˜ (5๊ฐœ)~~ โœ… Phase 1.5๋กœ ์ด๋™ +- [ ] ~~MultiConnectionQueryService ์ „ํ™˜ (4๊ฐœ)~~ โœ… Phase 1 ์™„๋ฃŒ - [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์‹คํ–‰ ### **Phase 3: ๊ด€๋ฆฌ ๊ธฐ๋Šฅ (2.5์ฃผ) - 162๊ฐœ ํ˜ธ์ถœ**