// src/auth/jwt/jwt.service.ts  
import { Injectable } from '@nestjs/common';  
import { JwtService } from '@nestjs/jwt';  
import { ConfigService } from '@nestjs/config';  
import { UsersService } from '../../users/users.service';  
  
export interface JwtPayload {  
  sub: string; // 사용자 ID  email?: string; // 이메일 (선택)  
  provider: string; // OAuth 제공자  
  type: 'access' | 'refresh'; // 토큰 타입  
  iat?: number; // 발급 시간  
  exp?: number; // 만료 시간  
}  
  
export interface TokenResponse {  
  accessToken: string;  
  refreshToken: string;  
  expiresIn: number;  
}  
  
@Injectable()  
export class AuthJwtService {  
  constructor(  
    private readonly jwtService: JwtService,  
    private readonly configService: ConfigService,  
  ) {}  
  
  /**  
   * 액세스 토큰과 리프레시 토큰 생성  
   */  
  async generateTokens(  
    payload: Omit<JwtPayload, 'type' | 'iat' | 'exp'>,  
  ): Promise<TokenResponse> {  
    const accessTokenExpiresIn = this.configService.get<number>(  
      'auth.jwt.accessExpiresIn',  
      900,  
    ); // 15분  
    const refreshTokenExpiresIn = this.configService.get<number>(  
      'auth.jwt.refreshExpiresIn',  
      604800,  
    ); // 7일  
  
    const [accessToken, refreshToken] = await Promise.all([  
      this.jwtService.signAsync(  
        {  
          ...payload,  
          type: 'access',  
        },  
        {  
          expiresIn: accessTokenExpiresIn,  
        },  
      ),  
      this.jwtService.signAsync(  
        {  
          ...payload,  
          type: 'refresh',  
        },  
        {  
          expiresIn: refreshTokenExpiresIn,  
        },  
      ),  
    ]);  
  
    return {  
      accessToken,  
      refreshToken,  
      expiresIn: accessTokenExpiresIn,  
    };  
  }  
  
  /**  
   * 토큰 검증  
   */  
  async verifyToken(token: string): Promise<JwtPayload> {  
    try {  
      return await this.jwtService.verifyAsync<JwtPayload>(token);  
    } catch (error) {  
      throw new Error(`Token verification failed: ${error.message}`);  
    }  
  }  
  
  /**  
   * 토큰 디코딩 (검증 없이)  
   */  decodeToken(token: string): JwtPayload {  
    try {  
      return this.jwtService.decode(token) as JwtPayload;  
    } catch (error) {  
      throw new Error(`Token decoding failed: ${error.message}`);  
    }  
  }  
  
  /**  
   * 토큰 만료 시간 확인  
   */  
  isTokenExpired(token: string): boolean {  
    try {  
      const decoded = this.decodeToken(token);  
      if (!decoded.exp) return true;  
      return decoded.exp * 1000 < Date.now();  
    } catch {  
      return true;  
    }  
  }  
  
  /**  
   * 토큰 남은 시간 확인 (초 단위)  
   */  getTokenTimeRemaining(token: string): number {  
    try {  
      const decoded = this.decodeToken(token);  
      if (!decoded.exp) return 0;  
      return Math.max(0, decoded.exp - Math.floor(Date.now() / 1000));  
    } catch {  
      return 0;  
    }  
  }  
}  
  
// JWT 서비스 사용 예시  
@Injectable()  
export class AuthService {  
  constructor(  
    private readonly jwtService: AuthJwtService,  
    private readonly usersService: UsersService,  
  ) {}  
  
  /**  
   * OAuth 로그인 처리  
   */  
  async handleOAuthLogin(  
    provider: string,  
    profile: any,  
  ): Promise<TokenResponse> {  
    // 1. 사용자 찾기 또는 생성  
    const user = await this.usersService.findOrCreateByOAuth(provider, profile);  
  
    // 2. JWT 토큰 생성  
    return this.jwtService.generateTokens({  
      sub: user.id,  
      email: user.email,  
      provider,  
    });  
  }  
  
  /**  
   * 토큰 갱신  
   */  
  async refreshToken(refreshToken: string): Promise<TokenResponse> {  
    try {  
      // 1. 리프레시 토큰 검증  
      const payload = await this.jwtService.verifyToken(refreshToken);  
  
      // 2. 리프레시 토큰 타입 확인  
      if (payload.type !== 'refresh') {  
        throw new Error('Invalid token type');  
      }  
  
      // 3. 사용자 확인  
      const user = await this.usersService.findById(payload.sub);  
      if (!user) {  
        throw new Error('User not found');  
      }  
  
      // 4. 새 토큰 발급  
      return this.jwtService.generateTokens({  
        sub: user.id,  
        email: user.email,  
        provider: payload.provider,  
      });  
    } catch (error) {  
      throw new Error(`Token refresh failed: ${error.message}`);  
    }  
  }  
  
  /**  
   * 토큰으로 사용자 인증  
   */  
  async validateToken(token: string): Promise<any> {  
    try {  
      // 1. 토큰 검증  
      const payload = await this.jwtService.verifyToken(token);  
  
      // 2. 액세스 토큰 타입 확인  
      if (payload.type !== 'access') {  
        throw new Error('Invalid token type');  
      }  
  
      // 3. 만료 확인  
      if (this.jwtService.isTokenExpired(token)) {  
        throw new Error('Token has expired');  
      }  
  
      // 4. 사용자 조회  
      return this.usersService.findById(payload.sub);  
    } catch (error) {  
      throw new Error(`Token validation failed: ${error.message}`);  
    }  
  }  
}

사용예시

  
// JWT 서비스 사용 예시  
@Injectable()  
export class AuthService {  
  constructor(  
    private readonly jwtService: AuthJwtService,  
    private readonly usersService: UsersService,  
  ) {}  
  
  /**  
   * OAuth 로그인 처리  
   */  
  async handleOAuthLogin(  
    provider: string,  
    profile: any,  
  ): Promise<TokenResponse> {  
    // 1. 사용자 찾기 또는 생성  
    const user = await this.usersService.findOrCreateByOAuth(provider, profile);  
  
    // 2. JWT 토큰 생성  
    return this.jwtService.generateTokens({  
      sub: user.id,  
      email: user.email,  
      provider,  
    });  
  }  
  
  /**  
   * 토큰 갱신  
   */  
  async refreshToken(refreshToken: string): Promise<TokenResponse> {  
    try {  
      // 1. 리프레시 토큰 검증  
      const payload = await this.jwtService.verifyToken(refreshToken);  
  
      // 2. 리프레시 토큰 타입 확인  
      if (payload.type !== 'refresh') {  
        throw new Error('Invalid token type');  
      }  
  
      // 3. 사용자 확인  
      const user = await this.usersService.findById(payload.sub);  
      if (!user) {  
        throw new Error('User not found');  
      }  
  
      // 4. 새 토큰 발급  
      return this.jwtService.generateTokens({  
        sub: user.id,  
        email: user.email,  
        provider: payload.provider,  
      });  
    } catch (error) {  
      throw new Error(`Token refresh failed: ${error.message}`);  
    }  
  }  
  
  /**  
   * 토큰으로 사용자 인증  
   */  
  async validateToken(token: string): Promise<any> {  
    try {  
      // 1. 토큰 검증  
      const payload = await this.jwtService.verifyToken(token);  
  
      // 2. 액세스 토큰 타입 확인  
      if (payload.type !== 'access') {  
        throw new Error('Invalid token type');  
      }  
  
      // 3. 만료 확인  
      if (this.jwtService.isTokenExpired(token)) {  
        throw new Error('Token has expired');  
      }  
  
      // 4. 사용자 조회  
      return this.usersService.findById(payload.sub);  
    } catch (error) {  
      throw new Error(`Token validation failed: ${error.message}`);  
    }  
  }  
}