# ๐Ÿ” 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์ผ **๋‹ด๋‹น์ž**: ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœํŒ€