# ๐Ÿ“‹ Phase 3.14: AuthService Raw Query ์ „ํ™˜ ๊ณ„ํš ## ๐Ÿ“‹ ๊ฐœ์š” AuthService๋Š” **5๊ฐœ์˜ Prisma ํ˜ธ์ถœ**์ด ์žˆ์œผ๋ฉฐ, ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. ### ๐Ÿ“Š ๊ธฐ๋ณธ ์ •๋ณด | ํ•ญ๋ชฉ | ๋‚ด์šฉ | | --------------- | ------------------------------------------ | | ํŒŒ์ผ ์œ„์น˜ | `backend-node/src/services/authService.ts` | | ํŒŒ์ผ ํฌ๊ธฐ | 335 ๋ผ์ธ | | Prisma ํ˜ธ์ถœ | 0๊ฐœ (์ด๋ฏธ Phase 1.5์—์„œ ์ „ํ™˜ ์™„๋ฃŒ) | | **ํ˜„์žฌ ์ง„ํ–‰๋ฅ ** | **5/5 (100%)** โœ… **์ „ํ™˜ ์™„๋ฃŒ** | | ๋ณต์žก๋„ | ๋†’์Œ (๋ณด์•ˆ, ์•”ํ˜ธํ™”, ์„ธ์…˜ ๊ด€๋ฆฌ) | | ์šฐ์„ ์ˆœ์œ„ | ๐ŸŸก ์ค‘๊ฐ„ (Phase 3.14) | | **์ƒํƒœ** | โœ… **์™„๋ฃŒ** (Phase 1.5์—์„œ ์ด๋ฏธ ์™„๋ฃŒ) | ### ๐ŸŽฏ ์ „ํ™˜ ๋ชฉํ‘œ - โณ **5๊ฐœ ๋ชจ๋“  Prisma ํ˜ธ์ถœ์„ `db.ts`์˜ `query()`, `queryOne()` ํ•จ์ˆ˜๋กœ ๊ต์ฒด** - โณ ์‚ฌ์šฉ์ž ์ธ์ฆ ๊ธฐ๋Šฅ ์ •์ƒ ๋™์ž‘ - โณ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”/๊ฒ€์ฆ ์œ ์ง€ - โณ ์„ธ์…˜ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ์œ ์ง€ - โณ ๊ถŒํ•œ ๊ฒ€์ฆ ๊ธฐ๋Šฅ ์œ ์ง€ - โณ TypeScript ์ปดํŒŒ์ผ ์„ฑ๊ณต - โณ **Prisma import ์™„์ „ ์ œ๊ฑฐ** --- ## ๐Ÿ” ์˜ˆ์ƒ Prisma ์‚ฌ์šฉ ํŒจํ„ด ### ์ฃผ์š” ๊ธฐ๋Šฅ (5๊ฐœ ์˜ˆ์ƒ) #### 1. **์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ (์ธ์ฆ)** - findFirst or findUnique - ์ด๋ฉ”์ผ/์‚ฌ์šฉ์ž๋ช…์œผ๋กœ ์กฐํšŒ - ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ #### 2. **์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ** - findUnique - user_id ๊ธฐ์ค€ - ๊ถŒํ•œ ์ •๋ณด ํฌํ•จ #### 3. **์‚ฌ์šฉ์ž ์ƒ์„ฑ (ํšŒ์›๊ฐ€์ž…)** - create - ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” - ์ค‘๋ณต ๊ฒ€์‚ฌ #### 4. **๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ** - update - ๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ - ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” #### 5. **์„ธ์…˜ ๊ด€๋ฆฌ** - create, update, delete - ์„ธ์…˜ ํ† ํฐ ์ €์žฅ/์กฐํšŒ --- ## ๐Ÿ’ก ์ „ํ™˜ ์ „๋žต ### 1๋‹จ๊ณ„: ์ธ์ฆ ๊ด€๋ จ ์ „ํ™˜ (2๊ฐœ) - login() - ์‚ฌ์šฉ์ž ์กฐํšŒ + ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ - getUserInfo() - ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ### 2๋‹จ๊ณ„: ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ์ „ํ™˜ (2๊ฐœ) - createUser() - ์‚ฌ์šฉ์ž ์ƒ์„ฑ - changePassword() - ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ### 3๋‹จ๊ณ„: ์„ธ์…˜ ๊ด€๋ฆฌ ์ „ํ™˜ (1๊ฐœ) - manageSession() - ์„ธ์…˜ CRUD --- ## ๐Ÿ’ป ์ „ํ™˜ ์˜ˆ์‹œ ### ์˜ˆ์‹œ 1: ๋กœ๊ทธ์ธ (๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ) **๋ณ€๊ฒฝ ์ „**: ```typescript async login(username: string, password: string) { const user = await prisma.users.findFirst({ where: { OR: [ { username: username }, { email: username }, ], is_active: true, }, }); if (!user) { throw new Error("User not found"); } const isPasswordValid = await bcrypt.compare(password, user.password_hash); if (!isPasswordValid) { throw new Error("Invalid password"); } return user; } ``` **๋ณ€๊ฒฝ ํ›„**: ```typescript async login(username: string, password: string) { const user = await queryOne( `SELECT * FROM users WHERE (username = $1 OR email = $1) AND is_active = $2`, [username, true] ); if (!user) { throw new Error("User not found"); } const isPasswordValid = await bcrypt.compare(password, user.password_hash); if (!isPasswordValid) { throw new Error("Invalid password"); } return user; } ``` ### ์˜ˆ์‹œ 2: ์‚ฌ์šฉ์ž ์ƒ์„ฑ (๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”) **๋ณ€๊ฒฝ ์ „**: ```typescript async createUser(userData: CreateUserDto) { // ์ค‘๋ณต ๊ฒ€์‚ฌ const existing = await prisma.users.findFirst({ where: { OR: [ { username: userData.username }, { email: userData.email }, ], }, }); if (existing) { throw new Error("User already exists"); } // ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” const passwordHash = await bcrypt.hash(userData.password, 10); // ์‚ฌ์šฉ์ž ์ƒ์„ฑ const user = await prisma.users.create({ data: { username: userData.username, email: userData.email, password_hash: passwordHash, company_code: userData.company_code, }, }); return user; } ``` **๋ณ€๊ฒฝ ํ›„**: ```typescript async createUser(userData: CreateUserDto) { // ์ค‘๋ณต ๊ฒ€์‚ฌ const existing = await queryOne( `SELECT * FROM users WHERE username = $1 OR email = $2`, [userData.username, userData.email] ); if (existing) { throw new Error("User already exists"); } // ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” const passwordHash = await bcrypt.hash(userData.password, 10); // ์‚ฌ์šฉ์ž ์ƒ์„ฑ const user = await queryOne( `INSERT INTO users (username, email, password_hash, company_code, created_at, updated_at) VALUES ($1, $2, $3, $4, NOW(), NOW()) RETURNING *`, [userData.username, userData.email, passwordHash, userData.company_code] ); return user; } ``` ### ์˜ˆ์‹œ 3: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ **๋ณ€๊ฒฝ ์ „**: ```typescript async changePassword( userId: number, oldPassword: string, newPassword: string ) { const user = await prisma.users.findUnique({ where: { user_id: userId }, }); if (!user) { throw new Error("User not found"); } const isOldPasswordValid = await bcrypt.compare( oldPassword, user.password_hash ); if (!isOldPasswordValid) { throw new Error("Invalid old password"); } const newPasswordHash = await bcrypt.hash(newPassword, 10); await prisma.users.update({ where: { user_id: userId }, data: { password_hash: newPasswordHash }, }); } ``` **๋ณ€๊ฒฝ ํ›„**: ```typescript async changePassword( userId: number, oldPassword: string, newPassword: string ) { const user = await queryOne( `SELECT * FROM users WHERE user_id = $1`, [userId] ); if (!user) { throw new Error("User not found"); } const isOldPasswordValid = await bcrypt.compare( oldPassword, user.password_hash ); if (!isOldPasswordValid) { throw new Error("Invalid old password"); } const newPasswordHash = await bcrypt.hash(newPassword, 10); await query( `UPDATE users SET password_hash = $1, updated_at = NOW() WHERE user_id = $2`, [newPasswordHash, userId] ); } ``` --- ## ๐Ÿ”ง ๊ธฐ์ˆ ์  ๊ณ ๋ ค์‚ฌํ•ญ ### 1. ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณด์•ˆ ```typescript import bcrypt from "bcrypt"; // ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ (ํšŒ์›๊ฐ€์ž…, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ) const SALT_ROUNDS = 10; const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ (๋กœ๊ทธ์ธ) const isValid = await bcrypt.compare(plainPassword, passwordHash); ``` ### 2. SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ```typescript // โŒ ์œ„ํ—˜: ์ง์ ‘ ๋ฌธ์ž์—ด ๊ฒฐํ•ฉ const sql = `SELECT * FROM users WHERE username = '${username}'`; // โœ… ์•ˆ์ „: ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ const user = await queryOne(`SELECT * FROM users WHERE username = $1`, [ username, ]); ``` ### 3. ์„ธ์…˜ ํ† ํฐ ๊ด€๋ฆฌ ```typescript import crypto from "crypto"; // ์„ธ์…˜ ํ† ํฐ ์ƒ์„ฑ const sessionToken = crypto.randomBytes(32).toString("hex"); // ์„ธ์…˜ ์ €์žฅ await query( `INSERT INTO user_sessions (user_id, session_token, expires_at) VALUES ($1, $2, NOW() + INTERVAL '1 day')`, [userId, sessionToken] ); ``` ### 4. ๊ถŒํ•œ ๊ฒ€์ฆ ```typescript async checkPermission(userId: number, permission: string): Promise { const result = await queryOne<{ has_permission: boolean }>( `SELECT EXISTS ( SELECT 1 FROM user_permissions up JOIN permissions p ON up.permission_id = p.permission_id WHERE up.user_id = $1 AND p.permission_name = $2 ) as has_permission`, [userId, permission] ); return result?.has_permission || false; } ``` --- ## โœ… ์ „ํ™˜ ์™„๋ฃŒ ๋‚ด์—ญ (Phase 1.5์—์„œ ์ด๋ฏธ ์™„๋ฃŒ๋จ) AuthService๋Š” Phase 1.5์—์„œ ์ด๋ฏธ Raw Query๋กœ ์ „ํ™˜์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ### ์ „ํ™˜๋œ Prisma ํ˜ธ์ถœ (5๊ฐœ) 1. **`loginPwdCheck()`** - ๋กœ๊ทธ์ธ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ - user_info ํ…Œ์ด๋ธ”์—์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์กฐํšŒ - EncryptUtil์„ ํ™œ์šฉํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ - ๋งˆ์Šคํ„ฐ ํŒจ์Šค์›Œ๋“œ ์ง€์› 2. **`insertLoginAccessLog()`** - ๋กœ๊ทธ์ธ ๋กœ๊ทธ ๊ธฐ๋ก - login_access_log ํ…Œ์ด๋ธ”์— INSERT - ๋กœ๊ทธ์ธ ์‹œ๊ฐ„, IP ์ฃผ์†Œ ๋“ฑ ๊ธฐ๋ก 3. **`getUserInfo()`** - ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ - user_info ํ…Œ์ด๋ธ” ์กฐํšŒ - PersonBean ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜ 4. **`updateLastLoginDate()`** - ๋งˆ์ง€๋ง‰ ๋กœ๊ทธ์ธ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ - user_info ํ…Œ์ด๋ธ” UPDATE - last_login_date ๊ฐฑ์‹  5. **`checkUserPermission()`** - ์‚ฌ์šฉ์ž ๊ถŒํ•œ ํ™•์ธ - user_auth ํ…Œ์ด๋ธ” ์กฐํšŒ - ๊ถŒํ•œ ์ฝ”๋“œ ๊ฒ€์ฆ ### ์ฃผ์š” ๊ธฐ์ˆ ์  ํŠน์ง• - **๋ณด์•ˆ**: EncryptUtil์„ ํ™œ์šฉํ•œ ์•ˆ์ „ํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ - **JWT ํ† ํฐ**: JwtUtils๋ฅผ ํ™œ์šฉํ•œ ํ† ํฐ ์ƒ์„ฑ ๋ฐ ๊ฒ€์ฆ - **๋กœ๊น…**: ์ƒ์„ธํ•œ ๋กœ๊ทธ์ธ ์ด๋ ฅ ๊ธฐ๋ก - **์—๋Ÿฌ ์ฒ˜๋ฆฌ**: ์•ˆ์ „ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜ ### ์ฝ”๋“œ ์ƒํƒœ - [x] Prisma import ์—†์Œ - [x] query ํ•จ์ˆ˜ ์‚ฌ์šฉ ์ค‘ - [x] TypeScript ์ปดํŒŒ์ผ ์„ฑ๊ณต - [x] ๋ณด์•ˆ ๋กœ์ง ์œ ์ง€ ## ๐Ÿ“ ์›๋ณธ ์ „ํ™˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ### 1๋‹จ๊ณ„: Prisma ํ˜ธ์ถœ ์ „ํ™˜ (โœ… Phase 1.5์—์„œ ์™„๋ฃŒ) - [ ] `login()` - ์‚ฌ์šฉ์ž ์กฐํšŒ + ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ (findFirst) - [ ] `getUserInfo()` - ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ (findUnique) - [ ] `createUser()` - ์‚ฌ์šฉ์ž ์ƒ์„ฑ (create with ์ค‘๋ณต ๊ฒ€์‚ฌ) - [ ] `changePassword()` - ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ (findUnique + update) - [ ] `manageSession()` - ์„ธ์…˜ ๊ด€๋ฆฌ (create/update/delete) ### 2๋‹จ๊ณ„: ๋ณด์•ˆ ๊ฒ€์ฆ - [ ] ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ ๋กœ์ง ์œ ์ง€ (bcrypt) - [ ] SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ํ™•์ธ - [ ] ์„ธ์…˜ ํ† ํฐ ๋ณด์•ˆ ํ™•์ธ - [ ] ์ค‘๋ณต ๊ณ„์ • ๋ฐฉ์ง€ ํ™•์ธ ### 3๋‹จ๊ณ„: ํ…Œ์ŠคํŠธ - [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (5๊ฐœ) - [ ] ๋กœ๊ทธ์ธ ์„ฑ๊ณต/์‹คํŒจ ํ…Œ์ŠคํŠธ - [ ] ์‚ฌ์šฉ์ž ์ƒ์„ฑ ํ…Œ์ŠคํŠธ - [ ] ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ํ…Œ์ŠคํŠธ - [ ] ์„ธ์…˜ ๊ด€๋ฆฌ ํ…Œ์ŠคํŠธ - [ ] ๊ถŒํ•œ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ - [ ] ๋ณด์•ˆ ํ…Œ์ŠคํŠธ - [ ] SQL ์ธ์ ์…˜ ํ…Œ์ŠคํŠธ - [ ] ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฐ•๋„ ํ…Œ์ŠคํŠธ - [ ] ์„ธ์…˜ ํƒˆ์ทจ ๋ฐฉ์ง€ ํ…Œ์ŠคํŠธ - [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (2๊ฐœ) ### 4๋‹จ๊ณ„: ๋ฌธ์„œํ™” - [ ] ์ „ํ™˜ ์™„๋ฃŒ ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ - [ ] ๋ณด์•ˆ ๊ฐ€์ด๋“œ ์—…๋ฐ์ดํŠธ --- ## ๐ŸŽฏ ์˜ˆ์ƒ ๋‚œ์ด๋„ ๋ฐ ์†Œ์š” ์‹œ๊ฐ„ - **๋‚œ์ด๋„**: โญโญโญโญ (๋†’์Œ) - ๋ณด์•ˆ ํฌ๋ฆฌํ‹ฐ์ปฌ (๋น„๋ฐ€๋ฒˆํ˜ธ, ์„ธ์…˜) - SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ํ•„์ˆ˜ - ์ฒ ์ €ํ•œ ํ…Œ์ŠคํŠธ ํ•„์š” - **์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 1.5~2์‹œ๊ฐ„ - Prisma ํ˜ธ์ถœ ์ „ํ™˜: 40๋ถ„ - ๋ณด์•ˆ ๊ฒ€์ฆ: 40๋ถ„ - ํ…Œ์ŠคํŠธ: 40๋ถ„ --- ## โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ ### ๋ณด์•ˆ ํ•„์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ 1. โœ… ๋ชจ๋“  ์‚ฌ์šฉ์ž ์ž…๋ ฅ์€ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ ์‚ฌ์šฉ 2. โœ… ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ ˆ๋Œ€ ํ‰๋ฌธ ์ €์žฅ ๊ธˆ์ง€ (bcrypt ์‚ฌ์šฉ) 3. โœ… ์„ธ์…˜ ํ† ํฐ์€ ์ถฉ๋ถ„ํžˆ ๊ธธ๊ณ  ๋žœ๋คํ•ด์•ผ ํ•จ 4. โœ… ๋น„๋ฐ€๋ฒˆํ˜ธ ์‹คํŒจ ์‹œ ๊ตฌ์ฒด์  ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ๊ธˆ์ง€ ("User not found" vs "Invalid credentials") 5. โœ… ๋กœ๊ทธ์ธ ์‹คํŒจ ํšŸ์ˆ˜ ์ œํ•œ (Brute Force ๋ฐฉ์ง€) --- **์ƒํƒœ**: โณ **๋Œ€๊ธฐ ์ค‘** **ํŠน์ด์‚ฌํ•ญ**: ๋ณด์•ˆ ํฌ๋ฆฌํ‹ฐ์ปฌ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”, ์„ธ์…˜ ๊ด€๋ฆฌ ํฌํ•จ **โš ๏ธ ์ฃผ์˜**: ์ด ์„œ๋น„์Šค๋Š” ๋ณด์•ˆ์— ๋งค์šฐ ์ค‘์š”ํ•˜๋ฏ€๋กœ ์‹ ์ค‘ํ•œ ํ…Œ์ŠคํŠธ ํ•„์ˆ˜!