Skip to content

Commit

Permalink
fix: S3 이미지 경로 형식 오류 및 게시글 관련 수정
Browse files Browse the repository at this point in the history
fix: S3 이미지 경로 형식 오류 및 게시글 관련 수정
kimsudang authored Jan 14, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents e69deb6 + 7f507e3 commit f3df809
Showing 4 changed files with 94 additions and 57 deletions.
58 changes: 35 additions & 23 deletions src/common/s3/s3.service.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,43 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { S3ConfigService } from '../../config/s3.config';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { v4 as uuid } from 'uuid';

@Injectable()
export class S3Service {
private readonly s3Client: S3Client;
private readonly bucketName: string;
private readonly s3Client = this.s3ConfigService.getS3Client();
private readonly bucketName = this.s3ConfigService.getBucketName();
private readonly region = this.s3ConfigService.getRegion();

constructor(private readonly s3ConfigService: S3ConfigService) {
this.s3Client = this.s3ConfigService.getS3Client();
this.bucketName = this.s3ConfigService.getBucketName();

// 디버깅 로그
console.log('S3Service Initialized');
console.log('S3 Bucket Name:', this.bucketName);
console.log('[S3Service] Initialized:');
console.log(' Bucket Name:', this.bucketName);
console.log(' Region:', this.region);
}

async uploadFiles(files: Express.Multer.File[]): Promise<string[]> {
if (!files || files.length === 0) {
throw new Error('[S3Service] 업로드할 파일이 없습니다.');
}

console.log(
'[S3Service] Files received for upload:',
files.map((file) => file.originalname),
);

const uploadPromises = files.map((file) => this.uploadFile(file));
return await Promise.all(uploadPromises);
return Promise.all(uploadPromises);
}

async uploadFile(file: Express.Multer.File): Promise<string> {
const key = `${uuid()}-${file.originalname}`; // 고유한 파일 이름 생성

console.log('Uploading file to S3:');
console.log('File Name:', file.originalname);
console.log('MIME Type:', file.mimetype);
console.log('Buffer Size:', file.buffer ? file.buffer.length : 'undefined');
// 파일명에 포함된 한글/특수문자 등을 '_'로 치환
const sanitizedFileName = file.originalname.replace(/[^a-zA-Z0-9.\-_]/g, '_');
const key = `uploads/${Date.now()}-${uuid()}-${sanitizedFileName}`;

if (!file.buffer) {
throw new Error('File buffer is undefined.');
}
console.log('[S3Service] Preparing to upload file:');
console.log(' Original File Name:', file.originalname);
console.log(' Sanitized Key:', key);

const params = {
Bucket: this.bucketName,
@@ -42,14 +47,21 @@ export class S3Service {
};

try {
console.log('Sending file to S3 with key:', key);
console.log('[S3Service] Uploading file to S3 with key:', key);

await this.s3Client.send(new PutObjectCommand(params));
const fileUrl = `https://${this.bucketName}.s3.${this.s3ConfigService.getS3Client().config.region}.amazonaws.com/${key}`;
console.log('File successfully uploaded to S3:', fileUrl);

console.log('[S3Service] Type of region:', typeof this.region);
console.log('[S3Service] Value of region:', this.region);

// key가 URL에 안전하게 들어가도록 encodeURIComponent 적용
const fileUrl = `https://${this.bucketName}.s3.${this.region}.amazonaws.com/${encodeURIComponent(key)}`;

console.log('[S3Service] File successfully uploaded. URL:', fileUrl);
return fileUrl;
} catch (error) {
console.error('S3 upload error:', error);
throw new InternalServerErrorException('S3 upload failed');
console.error('[S3Service] S3 upload error:', error.message);
throw new InternalServerErrorException('File upload failed');
}
}
}
56 changes: 30 additions & 26 deletions src/config/s3.config.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,45 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { S3Client, PutObjectCommand, PutObjectCommandInput } from '@aws-sdk/client-s3';
import { S3Client } from '@aws-sdk/client-s3';

@Injectable()
export class S3ConfigService {
private readonly s3Client: S3Client;
private readonly bucketName: string;
private readonly region: string;

constructor(private readonly configService: ConfigService) {
this.bucketName = this.configService.get<string>('AWS_S3_BUCKET_NAME');
this.region = this.configService.get<string>('AWS_REGION');

console.log('[DEBUG] region from process.env:', this.region);
console.log('[DEBUG] typeof region:', typeof this.region);

if (!this.bucketName || this.bucketName.trim() === '') {
throw new Error('[S3ConfigService] S3 Bucket Name이 .env에 설정되지 않았습니다.');
}

// AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY도 .env에서 가져오되
// 값을 못 찾으면 Error나 원하는 처리
const accessKeyId = this.configService.get<string>('AWS_ACCESS_KEY_ID');
const secretAccessKey = this.configService.get<string>('AWS_SECRET_ACCESS_KEY');

if (!accessKeyId || !secretAccessKey) {
throw new Error('[S3ConfigService] AWS 자격 증명(accessKey, secretKey)이 설정되지 않았습니다.');
}

/// S3Client 생성
this.s3Client = new S3Client({
region: 'ap-northeast-2', // 직접 하드코딩
credentials: {
accessKeyId: this.configService.get<string>('AWS_ACCESS_KEY_ID'),
secretAccessKey: this.configService.get<string>('AWS_SECRET_ACCESS_KEY'),
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey,
},
region: this.configService.get<string>('AWS_REGION'),
});

this.bucketName = this.configService.get<string>('AWS_S3_BUCKET_NAME');

if (!this.bucketName) {
throw new Error('S3 Bucket Name is not defined in environment variables.');
}
// 디버깅 로그
console.log('[S3ConfigService] Initialized:');
console.log(' Bucket Name:', this.bucketName);
}

getS3Client(): S3Client {
@@ -31,22 +50,7 @@ export class S3ConfigService {
return this.bucketName;
}

async uploadFile(file: Express.Multer.File): Promise<string> {
const key = `uploads/${Date.now()}-${file.originalname}`;

const params: PutObjectCommandInput = {
Bucket: this.bucketName,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
};

try {
await this.s3Client.send(new PutObjectCommand(params));
return `https://${this.bucketName}.s3.${this.configService.get<string>('AWS_REGION')}.amazonaws.com/${key}`;
} catch (error) {
console.error('S3 upload error', error);
throw new Error('File upload failed');
}
getRegion(): string {
return this.region;
}
}
10 changes: 7 additions & 3 deletions src/modules/boards/boards.controller.ts
Original file line number Diff line number Diff line change
@@ -16,13 +16,13 @@ import {
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiQuery,
ApiParam,
ApiBody,
ApiBearerAuth,
ApiConsumes,
ApiOperation,
} from '@nestjs/swagger';
import { BoardsService } from './boards.service';
import { CreateBoardDto } from './dto/create-board.dto';
@@ -200,8 +200,12 @@ export class BoardsController {
})
@UseGuards(JwtAuthGuard)
@Patch(':boardId/edit')
async updateBoard(@Param('boardId') board_id: number, @Body() updateBoardDto: Partial<UpdateBoardDto>) {
const updatedBoard = await this.boardsService.updateBoard(board_id, updateBoardDto);
async updateBoard(
@Param('boardId') board_id: number,
@Body() updateBoardDto: Partial<UpdateBoardDto>,
@Req() request: Request, // 수정 시 유저 검증
) {
const updatedBoard = await this.boardsService.updateBoard(board_id, updateBoardDto, request.user.user_id);
return { message: '게시글이 수정되었습니다.', data: updatedBoard };
}

27 changes: 22 additions & 5 deletions src/modules/boards/boards.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, NotFoundException, InternalServerErrorException } from '@nestjs/common';
import { Injectable, NotFoundException, InternalServerErrorException, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { Board } from './entities/board.entity';
@@ -119,6 +119,7 @@ export class BoardsService {
'board.updated_date',
'image.image_url', // 썸네일 이미지 URL만 선택
'user.name', // 작성자 이름
'user.user_id',
'user.profile_image', // 작성자 프로필 이미지
])
.skip(skip)
@@ -147,6 +148,7 @@ export class BoardsService {
'image.image_url',
'image.is_thumbnail',
'user.name',
'user.user_id',
'user.profile_image',
])
.where('board.board_id = :board_id', { board_id })
@@ -158,10 +160,24 @@ export class BoardsService {
}

// 게시글 수정
async updateBoard(board_id: number, data: Partial<Board>): Promise<Board> {
const board = await this.getBoardById(board_id); // 기존 게시글 조회
Object.assign(board, data); // 데이터 병합
return await this.boardRepository.save(board); // 수정된 데이터 저장
async updateBoard(board_id: number, data: Partial<Board>, user_id: string): Promise<Board> {
// 1. 기존 게시글 조회
const board = await this.getBoardById(board_id);

// 2. 게시글이 존재하는지 확인 (getBoardById에서 이미 검사하지만, 혹시 모를 상황 대비)
if (!board) {
throw new NotFoundException('게시글을 찾을 수 없습니다.');
}

// 3. 작성자와 요청자가 동일한지 확인
if (board.user_id !== user_id) {
// 작성자가 아닌 경우
throw new ForbiddenException('게시글 수정 권한이 없습니다.');
}

// 4. 실제 수정 로직
Object.assign(board, data);
return await this.boardRepository.save(board);
}

// 사용자가 작성한 게시글 조회
@@ -180,6 +196,7 @@ export class BoardsService {
'board.created_date',
'board.updated_date',
'image.image_url', // 썸네일 URL
'user.user_id',
'user.name', // 작성자 이름
'user.profile_image', // 작성자 프로필 이미지
])

0 comments on commit f3df809

Please sign in to comment.