Files
vexplor/docs/NodeJS_Refactoring_Rules.md

34 KiB

Java Spring Boot → Node.js + TypeScript 리팩토링 가이드라인

📋 프로젝트 개요

목표

  • 기존 Java Spring Boot 백엔드를 Node.js + TypeScript로 완전 리팩토링
  • React 프론트엔드와의 완벽한 통합
  • 타입 안전성과 개발 생산성 향상

기술 스택

{
  "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. 의존성 주입 패턴

interface IUserService {
  getUsers(): Promise<User[]>;
  createUser(user: CreateUserDto): Promise<User>;
}

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 규칙

// ✅ 권장
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. 클래스 및 함수 정의

// ✅ 권장
export class UserService {
  constructor(private userRepository: UserRepository) {}

  async getUsers(params: GetUsersParams): Promise<UserResponse[]> {
    try {
      return await this.userRepository.findMany(params);
    } catch (error) {
      throw new ServiceError("사용자 목록 조회 실패", error);
    }
  }
}

3. 에러 처리

export class ServiceError extends Error {
  constructor(
    message: string,
    public originalError?: Error,
    public statusCode: number = 500
  ) {
    super(message);
    this.name = "ServiceError";
  }
}

🔐 인증 시스템 마이그레이션 가이드라인

1. 기존 인증 시스템 분석

현재 Java Spring Boot 인증 구조

// 기존 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 토큰으로 변경 (기능은 동일)

변경 사항

// 기존: 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 타입 정의 (기존 구조 유지)

// 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<T = any> {
  success: boolean;
  message?: string;
  data?: T;
  error?: {
    code: string;
    details?: any;
  };
}

3.2 비밀번호 암호화 (기존 로직 포팅)

// 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 토큰 관리

// src/utils/jwtUtils.ts
export class JwtUtils {
  static generateToken(userInfo: UserInfo): string {
    // PersonBean 정보를 JWT 페이로드로 변환
  }

  static verifyToken(token: string): UserInfo {
    // JWT 토큰 검증 및 사용자 정보 추출
  }
}

3.4 인증 미들웨어

// 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)

// src/controllers/authController.ts
export class AuthController {
  async login(req: Request, res: Response) {
    // 기존 ApiLoginController.login() 로직을 그대로 포팅
    // 1. 사용자 ID/비밀번호 검증
    // 2. 기존 loginPwdCheck 로직 사용
    // 3. 로그인 로그 기록
    // 4. JWT 토큰 발급 (세션 대체)
  }
}

3.6 인증 서비스

// src/services/authService.ts
export class AuthService {
  async loginPwdCheck(userId: string, password: string): Promise<LoginResult> {
    // 기존 LoginService.loginPwdCheck() 로직 포팅
  }

  async insertLoginAccessLog(logData: LoginLogData): Promise<void> {
    // 기존 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 테이블

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 테이블

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 테이블

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. 마이그레이션 관리

# 마이그레이션 생성
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 토큰 구조

interface JWTPayload {
  userId: string;
  userName: string;
  email: string;
  deptCode?: string;
  permissions: string[];
  iat: number;
  exp: number;
}

2. 인증 미들웨어

export const authenticateToken = async (
  req: Request,
  res: Response,
  next: NextFunction
): Promise<void> => {
  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 설계

// ✅ 권장
GET    /api/users              // 사용자 목록 조회
GET    /api/users/:id          // 특정 사용자 조회
POST   /api/users              // 사용자 생성
PUT    /api/users/:id          // 사용자 전체 수정
PATCH  /api/users/:id          // 사용자 부분 수정
DELETE /api/users/:id          // 사용자 삭제

2. 응답 형식 표준화

interface ApiResponse<T> {
  success: boolean;
  data?: T;
  message?: string;
  error?: {
    code: string;
    details?: any;
  };
  pagination?: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}

3. 컨트롤러 구현

export class UserController {
  constructor(private userService: UserService) {}

  async getUsers(
    req: Request,
    res: Response,
    next: NextFunction
  ): Promise<void> {
    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. 테스트 구조

describe("UserController", () => {
  let userController: UserController;
  let userService: jest.Mocked<UserService>;

  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. 환경별 설정

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. 로깅 규칙

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

{
  "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

{
  "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주) - 완료

  • Node.js + TypeScript 프로젝트 설정
  • 기존 데이터베이스 스키마 분석 (docs/Database_Schema_Collection.md 참고)
  • Prisma 스키마 설계 및 마이그레이션
  • 기본 인증 시스템 구현
  • 에러 처리 및 로깅 설정
  • 서버 실행 및 포트 설정 (8080 포트)

🔄 Phase 2: 핵심 API 개발 (4-6주) - 진행 중

Phase 2-1: 인증 시스템 마이그레이션 (2주) - 우선 진행

Phase 2-1A: 기본 인증 구조 (1주) - 완료

  • 기존 인증 타입 정의 (LoginRequest, UserInfo, ApiResponse)
  • 비밀번호 암호화 유틸리티 (기존 EncryptUtil 포팅)
  • JWT 토큰 관리 유틸리티
  • 인증 미들웨어 구현

Phase 2-1B: 핵심 인증 API (1주) - 완료

  • 로그인 API (POST /api/auth/login) - 기존 ApiLoginController.login() 포팅
  • 사용자 정보 API (GET /api/auth/me) - 기존 getCurrentUser() 포팅
  • 로그아웃 API (POST /api/auth/logout) - 기존 logout() 포팅
  • 인증 상태 확인 API (GET /api/auth/status) - 기존 checkAuthStatus() 포팅
  • 로그인 로그 기록 기능 (기존 insertLoginAccessLog() 포팅)
  • 데이터베이스 스키마 동기화 (Prisma db pull)
  • 실제 데이터베이스 연결 및 테스트
  • 대소문자 처리 문제 해결
  • 로그인 API 성공 테스트 완료

Phase 2-2: 사용자 관리 API (1주)

  • 사용자 목록 조회 API (user_info 테이블 기반)
  • 사용자 상세 조회 API
  • 사용자 생성/수정 API
  • 사용자 비밀번호 변경 API

Phase 2-2A: 메뉴 관리 API (완료 )

  • 관리자 메뉴 조회 API (GET /api/admin/menus) - 완료: 기존 AdminController.getAdminMenuList() 포팅
  • 사용자 메뉴 조회 API (GET /api/admin/user-menus) - 완료: 기존 AdminController.getUserMenuList() 포팅
  • 메뉴 정보 조회 API (GET /api/admin/menus/:menuId) - 완료: 기존 AdminController.getMenuInfo() 포팅
  • JWT 토큰 인증 미들웨어 적용
  • Prisma $queryRaw를 사용한 복잡한 재귀 쿼리 포팅
  • 환경변수 설정 및 데이터베이스 연결 문제 해결
  • 프론트엔드 API 클라이언트에 JWT 토큰 자동 추가
  • 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 공유 활용 (가장 간단)

// 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 (
    <AuthGuard requireAdmin={true}>
      <CompanyManagement />
    </AuthGuard>
  );
}

2. BroadcastChannel API 활용 (고급)

// utils/tabCommunication.ts
export class TabCommunication {
  private channel: BroadcastChannel;

  constructor() {
    this.channel = new BroadcastChannel("auth-channel");
  }

  // 토큰 요청
  requestToken(): Promise<string | null> {
    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. 쿠키 기반 토큰 (가장 안전)

// backend-node/src/controllers/authController.ts
static async login(req: Request, res: Response): Promise<void> {
  // 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 토큰 관리 모범 사례

프론트엔드 토큰 관리

// 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;
    }
  },
};

토큰 동기화 유틸리티

// 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;
    }
  },
};

관리자 레이아웃 토큰 확인

// app/(main)/admin/layout.tsx
export default function AdminLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const [isAuthorized, setIsAuthorized] = useState<boolean | null>(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 클라이언트 통일

// lib/api/user.ts - 수정 전 (fetch 사용)
async function apiCall<T = any>(
  endpoint: string,
  options: RequestInit = {}
): Promise<ApiResponse<T>> {
  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<string, any>) {
  try {
    const response = await apiClient.get("/admin/users", {
      params: params,
    });
    // 토큰 자동 추가됨
    return response.data;
  } catch (error) {
    console.error("❌ 사용자 목록 API 오류:", error);
    throw error;
  }
}

백엔드 토큰 검증

// 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 구현 완료, 어드민 메뉴 인증 문제 해결, 토큰 인증 문제 완전 해결)