# Java Spring Boot → Node.js + TypeScript 리팩토링 가이드라인 ## 📋 프로젝트 개요 ### 목표 - 기존 Java Spring Boot 백엔드를 Node.js + TypeScript로 완전 리팩토링 - React 프론트엔드와의 완벽한 통합 - 타입 안전성과 개발 생산성 향상 ### 기술 스택 ```json { "runtime": "Node.js ^20.10.0", "framework": "Express ^4.18.2", "language": "TypeScript ^5.3.3", "orm": "Prisma ^5.7.1", "database": "PostgreSQL ^8.11.3", "authentication": "JWT + Passport", "testing": "Jest + Supertest" } ``` ## 🏗️ 아키텍처 원칙 ### 1. 계층별 분리 ``` Controller → Service → Repository → Database ↓ ↓ ↓ ↓ 라우팅 비즈니스로직 데이터접근 PostgreSQL ``` ### 2. 의존성 주입 패턴 ```typescript interface IUserService { getUsers(): Promise; createUser(user: CreateUserDto): Promise; } class UserController { constructor(private userService: IUserService) {} } ``` ### 3. 타입 안전성 우선 - 모든 API 응답/요청에 TypeScript 인터페이스 정의 - Prisma 스키마 기반 타입 자동 생성 - 런타임 타입 검증 (Joi) ## 📁 프로젝트 구조 규칙 ### 디렉토리 구조 ``` src/ ├── config/ # 설정 파일 ├── controllers/ # HTTP 요청 처리 ├── services/ # 비즈니스 로직 ├── repositories/ # 데이터 접근 계층 ├── middleware/ # Express 미들웨어 ├── utils/ # 유틸리티 함수 ├── types/ # TypeScript 타입 정의 ├── validators/ # 입력 검증 스키마 └── app.ts # 애플리케이션 진입점 ``` ### 파일 명명 규칙 - **컨트롤러**: `{Domain}Controller.ts` (예: `UserController.ts`) - **서비스**: `{Domain}Service.ts` (예: `UserService.ts`) - **리포지토리**: `{Domain}Repository.ts` (예: `UserRepository.ts`) - **타입**: `{Domain}.types.ts` (예: `user.types.ts`) - **검증**: `{Domain}.validator.ts` (예: `user.validator.ts`) ## 💻 코딩 컨벤션 ### 1. TypeScript 규칙 ```typescript // ✅ 권장 interface CreateUserRequest { userName: string; email: string; password: string; deptCode?: string; } type UserResponse = { id: number; userName: string; email: string; status: "Y" | "N"; regDate: Date; }; // ❌ 금지 const user: any = {}; const userName: string = req.body.userName as string; ``` ### 2. 클래스 및 함수 정의 ```typescript // ✅ 권장 export class UserService { constructor(private userRepository: UserRepository) {} async getUsers(params: GetUsersParams): Promise { try { return await this.userRepository.findMany(params); } catch (error) { throw new ServiceError("사용자 목록 조회 실패", error); } } } ``` ### 3. 에러 처리 ```typescript export class ServiceError extends Error { constructor( message: string, public originalError?: Error, public statusCode: number = 500 ) { super(message); this.name = "ServiceError"; } } ``` ## 🔐 인증 시스템 마이그레이션 가이드라인 ### 1. 기존 인증 시스템 분석 #### **현재 Java Spring Boot 인증 구조** ```java // 기존 API 엔드포인트 POST /api/auth/login // 로그인 POST /api/auth/logout // 로그아웃 GET /api/auth/me // 현재 사용자 정보 GET /api/auth/status // 인증 상태 확인 // 기존 핵심 클래스 - ApiLoginController: REST API 기반 로그인 컨트롤러 - LoginService: 로그인 비즈니스 로직 - PersonBean: 사용자 정보 객체 - EncryptUtil: 비밀번호 암호화 유틸리티 ``` #### **기존 인증 플로우** 1. **로그인 검증**: `LoginService.loginPwdCheck()` - 사용자 ID/비밀번호 검증 2. **세션 관리**: `SessionManager` - HttpSession 기반 세션 관리 3. **로그 기록**: `insertLoginAccessLog()` - LOGIN_ACCESS_LOG 테이블에 접속 로그 4. **사용자 정보**: `PersonBean` - 세션에 저장되는 사용자 정보 객체 ### 2. Node.js 마이그레이션 전략 #### **기존 로직 유지 원칙** - ✅ 기존 비즈니스 로직 그대로 유지 - ✅ 기존 데이터베이스 구조 그대로 사용 - ✅ 기존 API 응답 형식 유지 - ✅ 기존 로그인 플로우 유지 - 🔄 세션 → JWT 토큰으로 변경 (기능은 동일) #### **변경 사항** ```typescript // 기존: HttpSession 기반 HttpSession session = request.getSession(); session.setAttribute(Constants.PERSON_BEAN, person); // 변경: JWT 토큰 기반 const token = jwt.sign({ userId: user.userId, ...userInfo }, secret); ``` ### 3. 인증 시스템 구현 계획 #### **Phase 2-1A: 기본 인증 구조 (1주)** **3.1 타입 정의 (기존 구조 유지)** ```typescript // src/types/auth.ts export interface LoginRequest { userId: string; password: string; } export interface UserInfo { userId: string; userName: string; deptName: string; companyCode: string; companyName: string; } export interface ApiResponse { success: boolean; message?: string; data?: T; error?: { code: string; details?: any; }; } ``` **3.2 비밀번호 암호화 (기존 로직 포팅)** ```typescript // src/utils/passwordUtils.ts // 기존 EncryptUtil.encrypt() 로직을 Node.js로 포팅 export class PasswordUtils { static encrypt(password: string): string { // 기존 Java EncryptUtil 로직을 그대로 포팅 } static matches(plainPassword: string, encryptedPassword: string): boolean { // 기존 Java EncryptUtil.matches() 로직 포팅 } } ``` **3.3 JWT 토큰 관리** ```typescript // src/utils/jwtUtils.ts export class JwtUtils { static generateToken(userInfo: UserInfo): string { // PersonBean 정보를 JWT 페이로드로 변환 } static verifyToken(token: string): UserInfo { // JWT 토큰 검증 및 사용자 정보 추출 } } ``` **3.4 인증 미들웨어** ```typescript // src/middleware/authMiddleware.ts export const authenticateToken = ( req: Request, res: Response, next: NextFunction ) => { // JWT 토큰 검증하여 기존 세션 방식과 동일한 효과 // req.user에 사용자 정보 설정 (기존 PersonBean과 동일) }; ``` #### **Phase 2-1B: 핵심 인증 API (1주)** **3.5 로그인 API (`POST /api/auth/login`)** ```typescript // src/controllers/authController.ts export class AuthController { async login(req: Request, res: Response) { // 기존 ApiLoginController.login() 로직을 그대로 포팅 // 1. 사용자 ID/비밀번호 검증 // 2. 기존 loginPwdCheck 로직 사용 // 3. 로그인 로그 기록 // 4. JWT 토큰 발급 (세션 대체) } } ``` **3.6 인증 서비스** ```typescript // src/services/authService.ts export class AuthService { async loginPwdCheck(userId: string, password: string): Promise { // 기존 LoginService.loginPwdCheck() 로직 포팅 } async insertLoginAccessLog(logData: LoginLogData): Promise { // 기존 insertLoginAccessLog() 로직 포팅 } } ``` ### 4. 파일 구조 (기존 로직 유지) ``` backend-node/src/ ├── auth/ │ ├── authController.ts # ApiLoginController 포팅 │ ├── authService.ts # LoginService 포팅 │ └── authMiddleware.ts # JWT 기반 인증 ├── types/ │ ├── auth.ts # 기존 DTO 클래스 포팅 │ └── common.ts # 기존 ApiResponse 포팅 ├── utils/ │ ├── passwordUtils.ts # EncryptUtil 포팅 │ ├── jwtUtils.ts # JWT 토큰 관리 │ └── logger.ts # 기존 로깅 로직 └── routes/ └── authRoutes.ts # 기존 API 엔드포인트 ``` ### 5. 기존 로직 포팅 우선순위 #### **우선순위 1: 핵심 인증 로직** 1. **비밀번호 암호화 유틸리티** (EncryptUtil 포팅) 2. **로그인 검증 로직** (LoginService.loginPwdCheck 포팅) 3. **로그인 API** (ApiLoginController.login 포팅) #### **우선순위 2: 보조 기능** 1. **로그인 로그 기록** (insertLoginAccessLog 포팅) 2. **사용자 정보 조회** (getCurrentUser 포팅) 3. **로그아웃 API** (logout 포팅) ### 6. 테스트 전략 #### **기존 API 호환성 테스트** - 기존 Java API와 동일한 응답 형식 확인 - 기존 로그인 플로우 동작 확인 - 기존 로그 기록 기능 확인 #### **보안 테스트** - JWT 토큰 유효성 검증 - 비밀번호 암호화 정확성 확인 - Rate Limiting 동작 확인 ## 🗄️ 데이터베이스 설계 규칙 ### 1. 기존 데이터베이스 스키마 참고 **참고 문서**: `docs/Database_Schema_Collection.md` 이 문서에는 기존 PostgreSQL 데이터베이스의 완전한 스키마 정보가 포함되어 있습니다: - 전체 테이블 목록 (약 200개 테이블) - 각 테이블의 상세 컬럼 구조 - 외래키 관계 정보 - 인덱스 및 제약조건 정보 - 시퀀스, 뷰, 함수 정보 ### 2. 핵심 테이블 구조 #### **user_info 테이블** ```prisma model UserInfo { sabun String? @map("sabun") // 사번 userId String @unique @map("user_id") // 사용자 ID (NOT NULL) userPassword String? @map("user_password") // 비밀번호 userName String? @map("user_name") // 사용자명 userNameEng String? @map("user_name_eng") // 영문명 userNameCn String? @map("user_name_cn") // 중문명 deptCode String? @map("dept_code") // 부서코드 deptName String? @map("dept_name") // 부서명 positionCode String? @map("position_code") // 직급코드 positionName String? @map("position_name") // 직급명 // 관계 정의 userAuths UserAuth[] deptInfo DeptInfo? @relation(fields: [deptCode], references: [deptCode]) @@map("user_info") } ``` #### **menu_info 테이블** ```prisma model MenuInfo { objid Int @id @default(autoincrement()) @map("objid") // 객체ID (NOT NULL) menuType Int? @map("menu_type") // 메뉴타입 parentObjId Int? @map("parent_obj_id") // 부모객체ID menuNameKor String? @map("menu_name_kor") // 한글메뉴명 menuNameEng String? @map("menu_name_eng") // 영문메뉴명 seq Int? @map("seq") // 순서 menuUrl String? @map("menu_url") // 메뉴URL menuDesc String? @map("menu_desc") // 메뉴설명 writer String? @map("writer") // 작성자 regdate DateTime? @map("regdate") // 등록일 // 관계 정의 parent MenuInfo? @relation("MenuToMenu", fields: [parentObjId], references: [objid]) children MenuInfo[] @relation("MenuToMenu") menuAuthGroups MenuAuthGroup[] @@map("menu_info") } ``` #### **dept_info 테이블** ```prisma model DeptInfo { deptCode String @id @map("dept_code") // 부서코드 (NOT NULL) parentDeptCode String? @map("parent_dept_code") // 상위부서코드 deptName String? @map("dept_name") // 부서명 masterSabun String? @map("master_sabun") // 마스터사번 masterUserId String? @map("master_user_id") // 마스터사용자ID location String? @map("location") // 위치 locationName String? @map("location_name") // 위치명 regdate DateTime? @map("regdate") // 등록일 dataType String? @map("data_type") // 데이터타입 // 관계 정의 parent DeptInfo? @relation("DeptToDept", fields: [parentDeptCode], references: [deptCode]) children DeptInfo[] @relation("DeptToDept") users UserInfo[] @@map("dept_info") } ``` ### 3. 주요 테이블 카테고리 #### **사용자/권한 관련** - `user_info` - 사용자 정보 - `user_info_history` - 사용자 정보 히스토리 - `dept_info` - 부서 정보 - `authority_master` - 권한 마스터 - `rel_menu_auth` - 메뉴 권한 관계 #### **메뉴/시스템 관련** - `menu_info` - 메뉴 정보 - `table_labels` - 테이블 라벨 - `column_labels` - 컬럼 라벨 #### **다국어 관련** - `multi_lang_key_master` - 다국어 키 마스터 - `multi_lang_text` - 다국어 텍스트 - `language_master` - 언어 마스터 #### **비즈니스 로직 관련** - `comm_code` - 공통 코드 - `company_mng` - 회사 관리 - `contract_mgmt` - 계약 관리 - `order_mgmt` - 주문 관리 - `inventory_mgmt` - 재고 관리 - `part_mgmt` - 부품 관리 ### 4. 마이그레이션 관리 ```bash # 마이그레이션 생성 npx prisma migrate dev --name add_user_table # 마이그레이션 적용 npx prisma migrate deploy # 스키마 동기화 npx prisma db push ``` ### 5. 데이터 타입 매핑 규칙 | PostgreSQL | Prisma | TypeScript | | ----------------------------- | ---------- | ---------- | | `character varying` | `String` | `string` | | `numeric` | `Int` | `number` | | `timestamp without time zone` | `DateTime` | `Date` | | `boolean` | `Boolean` | `boolean` | | `text` | `String` | `string` | ## 🔐 인증 및 보안 규칙 ### 1. JWT 토큰 구조 ```typescript interface JWTPayload { userId: string; userName: string; email: string; deptCode?: string; permissions: string[]; iat: number; exp: number; } ``` ### 2. 인증 미들웨어 ```typescript export const authenticateToken = async ( req: Request, res: Response, next: NextFunction ): Promise => { try { const authHeader = req.headers.authorization; const token = authHeader?.split(" ")[1]; if (!token) { throw new AuthError("토큰이 제공되지 않았습니다", 401); } const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload; req.user = decoded; next(); } catch (error) { next(new AuthError("유효하지 않은 토큰입니다", 401)); } }; ``` ## 🌐 API 설계 규칙 ### 1. RESTful API 설계 ```typescript // ✅ 권장 GET /api/users // 사용자 목록 조회 GET /api/users/:id // 특정 사용자 조회 POST /api/users // 사용자 생성 PUT /api/users/:id // 사용자 전체 수정 PATCH /api/users/:id // 사용자 부분 수정 DELETE /api/users/:id // 사용자 삭제 ``` ### 2. 응답 형식 표준화 ```typescript interface ApiResponse { success: boolean; data?: T; message?: string; error?: { code: string; details?: any; }; pagination?: { page: number; limit: number; total: number; totalPages: number; }; } ``` ### 3. 컨트롤러 구현 ```typescript export class UserController { constructor(private userService: UserService) {} async getUsers( req: Request, res: Response, next: NextFunction ): Promise { try { const { page = 1, limit = 10, search } = req.query; const params: GetUsersParams = { page: Number(page), limit: Number(limit), search: search as string, }; const result = await this.userService.getUsers(params); res.json( successResponse(result.data, "사용자 목록을 성공적으로 조회했습니다") ); } catch (error) { next(error); } } } ``` ## 🧪 테스트 규칙 ### 1. 테스트 구조 ```typescript describe("UserController", () => { let userController: UserController; let userService: jest.Mocked; beforeEach(() => { userService = createMockUserService(); userController = new UserController(userService); }); describe("getUsers", () => { it("should return users list successfully", async () => { const mockUsers = [createMockUser()]; userService.getUsers.mockResolvedValue({ data: mockUsers, pagination: { page: 1, limit: 10, total: 1, totalPages: 1 }, }); const req = createMockRequest({ page: 1, limit: 10 }); const res = createMockResponse(); const next = jest.fn(); await userController.getUsers(req, res, next); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ success: true, data: mockUsers, }) ); }); }); }); ``` ## 🚀 배포 및 운영 규칙 ### 1. 환경별 설정 ```typescript interface Environment { nodeEnv: "development" | "production" | "test"; port: number; database: { url: string; pool: { min: number; max: number; }; }; jwt: { secret: string; expiresIn: string; }; cors: { origin: string[]; }; } ``` ### 2. 로깅 규칙 ```typescript export const logger = winston.createLogger({ level: process.env.LOG_LEVEL || "info", format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new winston.transports.File({ filename: "logs/error.log", level: "error" }), new winston.transports.File({ filename: "logs/combined.log" }), ], }); ``` ## 📦 필수 패키지 목록 ### Core Dependencies ```json { "express": "^4.18.2", "prisma": "^5.7.1", "@prisma/client": "^5.7.1", "pg": "^8.11.3", "jsonwebtoken": "^9.0.2", "bcryptjs": "^2.4.3", "helmet": "^7.1.0", "cors": "^2.8.5", "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.7", "winston": "^3.11.0", "joi": "^17.11.0", "redis": "^4.6.10", "compression": "^1.7.4", "express-rate-limit": "^7.1.5", "dotenv": "^16.3.1" } ``` ### Dev Dependencies ```json { "typescript": "^5.3.3", "@types/node": "^20.10.5", "@types/express": "^4.17.21", "@types/pg": "^8.10.9", "@types/jsonwebtoken": "^9.0.5", "@types/bcryptjs": "^2.4.6", "@types/cors": "^2.8.17", "@types/multer": "^1.4.11", "@types/nodemailer": "^6.4.14", "@types/morgan": "^1.9.9", "@types/compression": "^1.7.5", "@types/sanitize-html": "^2.9.5", "@types/node-cron": "^3.0.11", "@types/fs-extra": "^11.0.4", "@types/csv-parser": "^1.2.3", "jest": "^29.7.0", "@types/jest": "^29.5.11", "supertest": "^6.3.3", "@types/supertest": "^6.0.2", "ts-jest": "^29.1.1", "nodemon": "^3.0.2", "ts-node": "^10.9.2", "eslint": "^8.55.0", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "prettier": "^3.1.0" } ``` ## 📋 마이그레이션 체크리스트 ### ✅ Phase 1: 기반 구축 (1-2주) - **완료** - [x] Node.js + TypeScript 프로젝트 설정 - [x] 기존 데이터베이스 스키마 분석 (`docs/Database_Schema_Collection.md` 참고) - [x] Prisma 스키마 설계 및 마이그레이션 - [x] 기본 인증 시스템 구현 - [x] 에러 처리 및 로깅 설정 - [x] 서버 실행 및 포트 설정 (8080 포트) ### 🔄 Phase 2: 핵심 API 개발 (4-6주) - **진행 중** #### **Phase 2-1: 인증 시스템 마이그레이션 (2주) - 우선 진행** **Phase 2-1A: 기본 인증 구조 (1주) - ✅ 완료** - [x] 기존 인증 타입 정의 (`LoginRequest`, `UserInfo`, `ApiResponse`) - [x] 비밀번호 암호화 유틸리티 (기존 `EncryptUtil` 포팅) - [x] JWT 토큰 관리 유틸리티 - [x] 인증 미들웨어 구현 **Phase 2-1B: 핵심 인증 API (1주) - ✅ 완료** - [x] 로그인 API (`POST /api/auth/login`) - 기존 `ApiLoginController.login()` 포팅 - [x] 사용자 정보 API (`GET /api/auth/me`) - 기존 `getCurrentUser()` 포팅 - [x] 로그아웃 API (`POST /api/auth/logout`) - 기존 `logout()` 포팅 - [x] 인증 상태 확인 API (`GET /api/auth/status`) - 기존 `checkAuthStatus()` 포팅 - [x] 로그인 로그 기록 기능 (기존 `insertLoginAccessLog()` 포팅) - [x] 데이터베이스 스키마 동기화 (Prisma db pull) - [x] 실제 데이터베이스 연결 및 테스트 - [x] 대소문자 처리 문제 해결 - [x] 로그인 API 성공 테스트 완료 #### **Phase 2-2: 사용자 관리 API (1주)** - [ ] 사용자 목록 조회 API (`user_info` 테이블 기반) - [ ] 사용자 상세 조회 API - [ ] 사용자 생성/수정 API - [ ] 사용자 비밀번호 변경 API #### **Phase 2-2A: 메뉴 관리 API (완료 ✅)** - [x] 관리자 메뉴 조회 API (`GET /api/admin/menus`) - **완료: 기존 `AdminController.getAdminMenuList()` 포팅** - [x] 사용자 메뉴 조회 API (`GET /api/admin/user-menus`) - **완료: 기존 `AdminController.getUserMenuList()` 포팅** - [x] 메뉴 정보 조회 API (`GET /api/admin/menus/:menuId`) - **완료: 기존 `AdminController.getMenuInfo()` 포팅** - [x] JWT 토큰 인증 미들웨어 적용 - [x] Prisma $queryRaw를 사용한 복잡한 재귀 쿼리 포팅 - [x] 환경변수 설정 및 데이터베이스 연결 문제 해결 - [x] 프론트엔드 API 클라이언트에 JWT 토큰 자동 추가 - [x] 401/500 오류 해결 및 정상 작동 확인 #### **Phase 2-3: 부서 관리 API (1주)** - [ ] 부서 목록 조회 API (`dept_info` 테이블 기반) - [ ] 부서 트리 구조 API - [ ] 부서 생성/수정 API #### **Phase 2-4: 메뉴 및 권한 관리 API (1주)** - [ ] 메뉴 관리 API (`menu_info` 테이블 기반) - [ ] 권한 관리 API (`authority_master`, `rel_menu_auth` 테이블 기반) - [ ] 사용자별 메뉴 권한 조회 API #### **Phase 2-5: 다국어 및 공통 관리 API (1주)** - [ ] 다국어 관리 API (`multi_lang_key_master`, `multi_lang_text` 테이블 기반) - [ ] 공통 코드 관리 API (`comm_code` 테이블 기반) ### Phase 3: 비즈니스 로직 API (3-4주) - [ ] 회사 관리 API (`company_mng` 테이블 기반) - [ ] 계약 관리 API (`contract_mgmt` 테이블 기반) - [ ] 주문 관리 API (`order_mgmt` 테이블 기반) - [ ] 재고 관리 API (`inventory_mgmt` 테이블 기반) - [ ] 부품 관리 API (`part_mgmt` 테이블 기반) ### Phase 4: 고급 기능 (2-3주) - [ ] 파일 업로드/다운로드 (`attach_file_info` 테이블 기반) - [ ] Excel 처리 기능 - [ ] 이메일 발송 기능 (`mail_log` 테이블 기반) - [ ] 배치 처리 기능 - [ ] 로그 관리 API (`login_access_log` 테이블 기반) ### Phase 5: 테스트 및 최적화 (1-2주) - [ ] 단위 테스트 작성 - [ ] 통합 테스트 작성 - [ ] 성능 최적화 - [ ] 보안 검증 ### Phase 6: 배포 및 운영 (1주) - [ ] Docker 컨테이너화 - [ ] CI/CD 파이프라인 구축 - [ ] 모니터링 설정 - [ ] 문서화 ## ⚠️ 주의사항 1. **기존 데이터 보존**: 마이그레이션 시 기존 데이터 손실 방지 2. **API 호환성**: 프론트엔드와의 호환성 유지 3. **보안**: 인증/인가 로직 완전 재구현 4. **성능**: 데이터베이스 쿼리 최적화 5. **테스트**: 모든 기능에 대한 테스트 코드 작성 6. **스키마 참고**: `docs/Database_Schema_Collection.md` 문서를 항상 참고하여 정확한 테이블 구조 반영 7. **데이터 타입 매핑**: PostgreSQL → Prisma → TypeScript 타입 매핑 정확성 확인 8. **관계 설정**: 외래키 관계를 Prisma 관계로 정확히 매핑 9. **히스토리 테이블**: `*_history` 테이블들의 처리 방안 수립 10. **임시 테이블**: `temp*`, `*_temp` 테이블들의 정리 및 제거 계획 수립 11. **메뉴 API 완료**: `/api/admin/menus`와 `/api/admin/user-menus` API가 성공적으로 구현되어 프론트엔드 메뉴 표시가 정상 작동 12. **JWT 토큰 관리**: 프론트엔드 API 클라이언트에서 JWT 토큰을 자동으로 포함하여 인증 문제 해결 13. **환경변수 관리**: Prisma 스키마에서 직접 데이터베이스 URL 설정으로 환경변수 로딩 문제 해결 14. **어드민 메뉴 인증**: 새 탭에서 열리는 어드민 페이지의 토큰 인증 문제 해결 - localStorage 공유 활용 15. **관리자 메뉴 내 페이지 이동 토큰 문제**: 레이아웃 레벨 토큰 확인 및 동기화 구현 16. **API 클라이언트 통일**: 모든 API에서 apiClient 사용으로 토큰 자동 전달 보장 17. **토큰 동기화 유틸리티**: localStorage와 sessionStorage 간 토큰 동기화 및 복원 기능 ## 🔐 인증 및 보안 가이드 ### 어드민 메뉴 토큰 인증 문제 해결 #### 문제 상황 - 어드민 버튼 클릭 시 새 탭에서 어드민 페이지가 열림 - 새 탭에서 토큰 인증 문제 발생 가능성 - URL 파라미터로 토큰 전달은 보안상 위험 #### 해결 방안 (권장) **1. localStorage 공유 활용 (가장 간단)** ```typescript // AdminButton.tsx - 수정 없음 const handleAdminClick = () => { const adminUrl = `${window.location.origin}/admin`; window.open(adminUrl, "_blank"); }; // admin/page.tsx - AuthGuard 적용 ("use client"); import { AuthGuard } from "@/components/auth/AuthGuard"; import { CompanyManagement } from "@/components/admin/CompanyManagement"; export default function AdminPage() { return ( ); } ``` **2. BroadcastChannel API 활용 (고급)** ```typescript // utils/tabCommunication.ts export class TabCommunication { private channel: BroadcastChannel; constructor() { this.channel = new BroadcastChannel("auth-channel"); } // 토큰 요청 requestToken(): Promise { return new Promise((resolve) => { const timeout = setTimeout(() => { resolve(localStorage.getItem("authToken")); }, 100); this.channel.postMessage({ type: "REQUEST_TOKEN" }); const handler = (event: MessageEvent) => { if (event.data.type === "TOKEN_RESPONSE") { clearTimeout(timeout); this.channel.removeEventListener("message", handler); resolve(event.data.token); } }; this.channel.addEventListener("message", handler); }); } } ``` **3. 쿠키 기반 토큰 (가장 안전)** ```typescript // backend-node/src/controllers/authController.ts static async login(req: Request, res: Response): Promise { // HTTPOnly 쿠키로 토큰 설정 res.cookie('authToken', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 24 * 60 * 60 * 1000, // 24시간 }); } ``` #### 보안 고려사항 1. **URL 파라미터 사용 금지**: 토큰이 URL에 노출되어 보안 위험 2. **HTTPS 필수**: 프로덕션 환경에서는 반드시 HTTPS 사용 3. **토큰 만료 처리**: 자동 갱신 또는 재로그인 유도 4. **CSRF 방지**: 토큰 기반 요청 검증 5. **로그아웃 처리**: 모든 탭에서 토큰 제거 #### 구현 우선순위 **1단계 (즉시 적용)** - AuthGuard를 사용한 어드민 페이지 보호 - localStorage 공유 활용 **2단계 (1-2일 내)** - 토큰 유효성 검증 API 추가 - 에러 처리 개선 **3단계 (3-5일 내)** - 세션 관리 개선 - 토큰 갱신 로직 추가 ### JWT 토큰 관리 모범 사례 #### 프론트엔드 토큰 관리 ```typescript // lib/api/client.ts const TokenManager = { getToken: (): string | null => { if (typeof window !== "undefined") { return localStorage.getItem("authToken"); } return null; }, isTokenExpired: (token: string): boolean => { try { const payload = JSON.parse(atob(token.split(".")[1])); return payload.exp * 1000 < Date.now(); } catch { return true; } }, }; ``` #### 토큰 동기화 유틸리티 ```typescript // lib/sessionManager.ts export const tokenSync = { // 토큰 상태 확인 checkToken: () => { const token = localStorage.getItem("authToken"); console.log("🔍 토큰 상태 확인:", token ? "존재" : "없음"); return !!token; }, // 토큰 강제 동기화 (다른 탭에서 설정된 토큰을 현재 탭에 복사) forceSync: () => { const token = localStorage.getItem("authToken"); if (token) { // sessionStorage에도 복사 sessionStorage.setItem("authToken", token); console.log("🔄 토큰 강제 동기화 완료"); return true; } return false; }, // 토큰 복원 시도 (sessionStorage에서 복원) restoreFromSession: () => { const sessionToken = sessionStorage.getItem("authToken"); if (sessionToken) { localStorage.setItem("authToken", sessionToken); console.log("🔄 sessionStorage에서 토큰 복원 완료"); return true; } return false; }, // 토큰 유효성 검증 validateToken: (token: string) => { if (!token) return false; try { // JWT 토큰 구조 확인 (header.payload.signature) const parts = token.split("."); if (parts.length !== 3) return false; // payload 디코딩 시도 const payload = JSON.parse(atob(parts[1])); const now = Math.floor(Date.now() / 1000); // 만료 시간 확인 if (payload.exp && payload.exp < now) { console.log("❌ 토큰 만료됨"); return false; } console.log("✅ 토큰 유효성 검증 통과"); return true; } catch (error) { console.log("❌ 토큰 유효성 검증 실패:", error); return false; } }, }; ``` #### 관리자 레이아웃 토큰 확인 ```typescript // app/(main)/admin/layout.tsx export default function AdminLayout({ children, }: { children: React.ReactNode; }) { const [isAuthorized, setIsAuthorized] = useState(null); // 토큰 확인 및 인증 상태 체크 useEffect(() => { const checkToken = () => { const token = localStorage.getItem("authToken"); // 토큰이 없으면 sessionStorage에서 복원 시도 if (!token && sessionToken) { const restored = tokenSync.restoreFromSession(); if (restored) { setIsAuthorized(true); return; } } // 토큰 유효성 검증 if (token && !tokenSync.validateToken(token)) { localStorage.removeItem("authToken"); sessionStorage.removeItem("authToken"); setIsAuthorized(false); return; } if (!token) { setIsAuthorized(false); return; } // 토큰이 있으면 인증된 것으로 간주 setIsAuthorized(true); // 토큰 강제 동기화 (다른 탭과 동기화) tokenSync.forceSync(); }; // 초기 토큰 확인 checkToken(); // localStorage 변경 이벤트 리스너 추가 const handleStorageChange = (e: StorageEvent) => { if (e.key === "authToken") { checkToken(); } }; // 페이지 포커스 시 토큰 재확인 const handleFocus = () => { checkToken(); }; window.addEventListener("storage", handleStorageChange); window.addEventListener("focus", handleFocus); return () => { window.removeEventListener("storage", handleStorageChange); window.removeEventListener("focus", handleFocus); }; }, [pathname]); } ``` #### API 클라이언트 통일 ```typescript // lib/api/user.ts - 수정 전 (fetch 사용) async function apiCall( endpoint: string, options: RequestInit = {} ): Promise> { const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: { "Content-Type": "application/json", ...options.headers, }, credentials: "include", ...options, }); // 토큰 수동 추가 필요 } // lib/api/user.ts - 수정 후 (apiClient 사용) export async function getUserList(params?: Record) { try { const response = await apiClient.get("/admin/users", { params: params, }); // 토큰 자동 추가됨 return response.data; } catch (error) { console.error("❌ 사용자 목록 API 오류:", error); throw error; } } ``` #### 백엔드 토큰 검증 ```typescript // middleware/authMiddleware.ts export const authenticateToken = ( req: AuthenticatedRequest, res: Response, next: NextFunction ): void => { try { const authHeader = req.get("Authorization"); const token = authHeader && authHeader.split(" ")[1]; if (!token) { res.status(401).json({ success: false, error: { code: "TOKEN_MISSING", details: "인증 토큰이 필요합니다.", }, }); return; } const userInfo: PersonBean = JwtUtils.verifyToken(token); req.user = userInfo; next(); } catch (error) { res.status(401).json({ success: false, error: { code: "INVALID_TOKEN", details: "토큰 검증에 실패했습니다.", }, }); } }; ``` ### 토큰 인증 문제 해결 완료 사항 #### ✅ 해결된 문제들 1. **어드민 메뉴 토큰 인증 문제** - 새 탭에서 열리는 어드민 페이지의 토큰 공유 ✅ - localStorage 기반 토큰 동기화 ✅ 2. **관리자 메뉴 내 페이지 이동 시 토큰 문제** - 레이아웃 레벨에서 토큰 확인 로직 추가 ✅ - 실시간 토큰 동기화 및 검증 ✅ 3. **사용자 관리 메뉴 특정 인증 문제** - API 클라이언트 통일 (fetch → apiClient) ✅ - 토큰 자동 전달 활성화 ✅ #### 🔧 구현된 기능들 - **토큰 동기화 유틸리티**: `tokenSync` 모듈 - **강화된 인증 체크**: 레이아웃 레벨 토큰 검증 - **API 클라이언트 통일**: 모든 API에서 토큰 자동 전달 - **디버깅 도구**: 상세한 토큰 상태 확인 및 API 테스트 #### 📝 테스트 방법 1. **Admin 버튼 클릭** → 어드민 페이지 열기 2. **사이드바 메뉴 클릭** → 다른 관리자 페이지로 이동 3. **디버깅 페이지 확인** → `/admin/debug-layout`에서 토큰 상태 확인 4. **API 테스트** → 각 메뉴에서 API 호출 정상 작동 확인 ## 🎯 성공 지표 1. **성능 개선**: API 응답 시간 30% 단축 2. **개발 생산성**: 새로운 기능 개발 시간 50% 단축 3. **유지보수성**: 코드 복잡도 감소 4. **확장성**: 마이크로서비스 아키텍처 준비 --- **마지막 업데이트**: 2024년 12월 20일 **버전**: 1.9.0 **작성자**: AI Assistant **현재 상태**: Phase 1 완료, Phase 2-1A 완료, Phase 2-1B 완료, Phase 2-2A 완료 ✅ (메뉴 API 구현 완료, 어드민 메뉴 인증 문제 해결, 토큰 인증 문제 완전 해결)