제로부터 쌓는 개발일지
article thumbnail
반응형

나의 코드

src/configs/cloudflare/cloudflare.module.ts

더보기
// src/configs/cloudflare/cloudflare.module.ts
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { CloudflareService } from './cloudflare.service';

@Module({
  imports: [HttpModule],
  providers: [CloudflareService],
  exports: [CloudflareService],
})
export class CloudflareModule {}

 

src/configs/cloudflare/cloudflare.service.ts

더보기
// src/configs/cloudflare/cloudflare.service.ts
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import FormData from 'form-data';

@Injectable()
export class CloudflareService {
  constructor(
    private httpService: HttpService,
    private configService: ConfigService,
  ) {}

  async uploadImage(imageBuffer: Buffer, filename: string): Promise<string> {
    const cloudflareUrl = `https://api.cloudflare.com/client/v4/accounts/${this.configService.get(
      'ACCOUNT_ID',
    )}/images/v1`;

    const formData = new FormData();
    formData.append('file', imageBuffer, filename);

    const response = await this.httpService
      .post(cloudflareUrl, formData, {
        headers: {
          ...formData.getHeaders(),
          Authorization: `Bearer ${this.configService.get('API_TOKEN')}`,
        },
      })
      .toPromise();

    if (!response.data.success) {
      throw new Error('Failed to upload image to Cloudflare');
    }
    return response.data.result.url;
  }
}

 

src/users/dto/update-user.dto.ts

더보기
// src/users/dto/update-user.dto.ts
import { IsString, IsOptional, Length, IsNotEmpty } from 'class-validator';

export class UpdateUserDto {
  @IsString()
  @IsNotEmpty()
  currentPassword: string;

  @IsString()
  @IsOptional()
  @Length(6, 20)
  newPassword?: string;

  @IsString()
  @IsOptional()
  username?: string;

  @IsString()
  @IsOptional()
  imageUrl?: string;

  @IsString()
  @IsOptional()
  comment?: string;
}

 

src/users/users.module.ts

더보기
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CloudflareModule } from '../configs/cloudflare/cloudflare.module';
import { UsersController } from './users.controller';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';

@Module({
  imports: [TypeOrmModule.forFeature([User]), CloudflareModule],
  providers: [UsersService],
  exports: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

 

src/users/users.controller.ts

더보기
// src/users/users.controller.ts
import {
  Body,
  Controller,
  UseGuards,
  Request,
  Patch,
  UseInterceptors,
  UploadedFile,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { FileInterceptor } from '@nestjs/platform-express';
import { JwtAuthGuard } from '../auth/guard/jwt-auth.guard';
import { UpdateUserDto } from './dto/update-user.dto';
import { UsersService } from './users.service';

@Controller('user')
export class UsersController {
  constructor(
    private readonly userService: UsersService,
  ) {}

...

  @Patch()
  @UseGuards(AuthGuard('jwt'), JwtAuthGuard)
  @UseInterceptors(FileInterceptor('image'))
  async updateProfile(
    @Request() req,
    @Body() updateUserDto: UpdateUserDto,
    @UploadedFile() imageFile: Express.Multer.File,
  ) {
    const email = req.user.email;
    const updatedUser = await this.userService.updateUser(
      email,
      updateUserDto,
      imageFile,
    );
    return { message: '회원 정보가 업데이트되었습니다.', updatedUser };
  }
}

 

src/users/users.service.ts

더보기
// src/users/users.service.ts
import { ConflictException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcrypt';
import { Repository } from 'typeorm';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';
import { CloudflareService } from '../configs/cloudflare/cloudflare.service';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
    private cloudflareService: CloudflareService,
  ) {}

...

  async updateUser(
    email: string,
    updateUserDto: UpdateUserDto,
    imageFile?: Express.Multer.File,
  ): Promise<User> {
    const { currentPassword, newPassword, username, imageUrl, comment } =
      updateUserDto;

    const user = await this.userRepository.findOne({ where: { email } });
    if (!user) {
      throw new ConflictException('사용자를 찾을 수 없습니다.');
    }

    const passwordValid = await bcrypt.compare(currentPassword, user.password);
    if (!passwordValid) {
      throw new ConflictException('현재 비밀번호가 일치하지 않습니다.');
    }

    if (newPassword) {
      user.password = await bcrypt.hash(newPassword, 10);
    }
    if (username) {
      user.username = username;
    }

    if (comment) {
      user.comment = comment;
    }
    if (imageFile) {
      // 이미지 파일이 제공된 경우 Cloudflare에 업로드
      const uploadedImageUrl = await this.cloudflareService.uploadImage(
        imageFile.buffer,
        imageFile.originalname,
      );
      user.imageUrl = uploadedImageUrl;
    } else if (imageUrl) {
      // 이미지 URL이 직접 제공된 경우
      user.imageUrl = imageUrl;
    }

    return this.userRepository.save(user);
  }
}

 

 

 

CloudFlare에 업로드는 잘되지만 URL이 저장 안 되는 문제

 

이제 잘 되는 건가?! 했는데 어림도 없지

 

업로드는 되지만 URL이 DB에 저장 안 되는 문제가 발생했다.

 

그래서 콘솔로그를 찍어봤는데

 

 

아... 하? return을 이상하게 주고 있었다.

 

당연히 url인줄 알았는데 어 - 림도 없지 variants였다.

 

 

이예에에에!!!!!!

 

 

 

잘되긴 하는데 그치만.. 이건.. 생성이 아니라 수정인데...?

 

이미지가 업로드되는 것과 URL이 DB에 들어가는 건 잘되지만 뭔가 마음에 들지 않는다.

 

모름지기 수정이라 하면 기존 데이터를 덮어씌우는 건데 이미지 원본 파일의 경우 계속 추가가 돼버리네?

 

그래서 수정을 할 땐 기존에 올라가 있는 이미지 원본 파일을 지우고 새로운 파일을 업로드하는 방향으로 구현해 보기로 했다.

 

그러려면 먼저 삭제를 시킬 수 있는 기준을 찾아야 되는데.. 띠용?

 

 

CloudFlare의 이미지에 할당되어 있는 id가 이미지 URL에 야무지게 들어가 있는 걸 발견했다.

 

오.. 그러면 이미지 URL에서 id에 해당되는 부분을 잘라내서 쓰면 되지 않을까?!

 

...
  async deleteImage(imageUrl: string): Promise<void> {
    // Cloudflare 이미지 URL에서 이미지 ID 추출
    const urlParts = imageUrl.split('/');
    const imageIdIndex = urlParts.length - 2; // 'public' 바로 앞 부분이 이미지 ID
    const imageId = urlParts[imageIdIndex];

    console.log(imageId, '추출되냐 짜샤!');

    const cloudflareDeleteUrl = `https://api.cloudflare.com/client/v4/accounts/${this.configService.get(
      'ACCOUNT_ID',
    )}/images/v1/${imageId}`;
...

 

오... 생각했던 대로 하니까 되긴 됐는데... 어?

 

처음부터 다시 테스트해 본다고 CloudFlare에 있는 이미지를 싹 지웠는데 이미지가 존재하지 않는 경우엔 에러가 발생했다.

 

 

CloudFlare에서 요청된 id에 해당하는 이미지를 찾을 수 없다고 한다.

 

처음엔 코드를 잘못 짠 건가? 싶었는데 생각해 보니 CloudFlare에서 직접 이미지를 날려줬잖아?

그러면 DB에 있는 이미지 URL 정보도 지워주면 되는 게 아닐까?

 

 

캬ㅑㅑㅑㅑㅑㅑ 아주 잘된다

 

aws s3도 써봤지만 요놈은 프리티어여도 과금이 되는 기상천외한 상황이 발생해서 CloudFlare images로 옮겼는데

 

여러모로 괜찮은 듯하다.

 

물 - 론 무료는 아니지만 5불만 내면 이미지 100,000개 저장 가능해서 사실상 평생 쓴다 진자야

 

동일한 이름의 이미지 파일이 못 올라가게 해야 될 거 같기도 한데..

 

지금 가장 큰 문제는 API가 작동하는데 약 1~2초의 시간이 걸린다는 거다.

 

이미지파일이 존재하는지 찾고 있으면 삭제하고 재업로드 하는 형식이라 딜레이가 좀 걸리는듯한데..

 

사실 구현자체를 좀 잘못하긴 했다.

 

CloudFlare는 클라이언트단에서 서버로부터 api를 받아서 CloudFlare와 연결이 가능하고 여기서 받은 URL을 서버로 전송해 주는 로직을 지원해 주는데 

 

지금은 사실상 클라이언트가 서버로 이미지 파일을 넘기고 서버에서 CloudFlare와 통신하는 구조라.. 고민을 좀 해봐야 될듯하다.

 

그냥 트랜잭션 처리 해버릴까..!? 솔직히 수정할 깜냥은 못된다.. 진자야..

 

 

 

수정 & 새로 추가한 코드

src/configs/cloudflare/cloudflare.service.ts

더보기
// src/configs/cloudflare/cloudflare.service.ts
...
  async deleteImage(imageUrl: string): Promise<void> {
    // Cloudflare 이미지 URL에서 이미지 ID 추출
    const urlParts = imageUrl.split('/');
    const imageIdIndex = urlParts.length - 2;
    const imageId = urlParts[imageIdIndex];

    const cloudflareDeleteUrl = `https://api.cloudflare.com/client/v4/accounts/${this.configService.get(
      'ACCOUNT_ID',
    )}/images/v1/${imageId}`;

    const response = await this.httpService
      .delete(cloudflareDeleteUrl, {
        headers: {
          Authorization: `Bearer ${this.configService.get('API_TOKEN')}`,
        },
      })
      .toPromise();

    if (!response.data.success) {
      throw new Error('Failed to delete image from Cloudflare');
    }
  }
}
...

 

src/users/users.service.ts

더보기
...
  // 이미지 파일이 제공된 경우
    if (imageFile) {
      // 기존 이미지가 있으면 삭제
      if (user.imageUrl) {
        await this.cloudflareService.deleteImage(user.imageUrl);
      }

      // 새 이미지 업로드
      const uploadedImageUrl = await this.cloudflareService.uploadImage(
        imageFile.buffer,
        imageFile.originalname,
      );
      user.imageUrl = uploadedImageUrl;
    } else if (imageUrl) {
      // 이미지 URL이 직접 제공된 경우
      user.imageUrl = imageUrl;
    }
...

 

 

 

레퍼런스

[Typescript] Nest.js Cloudflare R2 + Images 통합 Multer storage engine 개발기

CloudFlare community

CloudFlare Docs

CloudFlare 문서 API Token

CloudFlare API

반응형
profile

제로부터 쌓는 개발일지

@PachyuChepe

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...