From e837ccc1d130a1ab504476c10aefc22568a6e324 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 30 Sep 2025 15:40:18 +0900 Subject: [PATCH] docs: Add Phase 1.5 for Auth and Admin service migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 진행 전 인증/관리자 시스템 우선 전환 계획 수립 🔐 Phase 1.5 추가: - AuthService 우선 전환 (5개 Prisma 호출) - AdminService 확인 (이미 Raw Query 사용) - AdminController 전환 (28개 Prisma 호출) 📋 변경 사항: - Phase 1.5를 Phase 2보다 우선 실행하도록 계획 변경 - 인증/관리자 시스템을 먼저 안정화한 후 핵심 서비스 전환 - 로그인 → 인증 → API 호출 전체 플로우 검증 📚 새 문서: - PHASE1.5_AUTH_MIGRATION_PLAN.md (상세 전환 계획) - AuthService 전환 방법 및 코드 예시 - 단위 테스트 및 통합 테스트 계획 - 완료 체크리스트 🎯 목표: - 전체 시스템의 안정적인 인증 기반 구축 - Phase 2 이후 모든 서비스가 안전하게 인증 시스템 사용 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- PHASE1.5_AUTH_MIGRATION_PLAN.md | 733 ++++++++++++++++++++++++++ PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md | 25 +- 2 files changed, 756 insertions(+), 2 deletions(-) create mode 100644 PHASE1.5_AUTH_MIGRATION_PLAN.md 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개 호출**