diff --git a/README.md b/README.md index 97606e3..edced3b 100644 --- a/README.md +++ b/README.md @@ -206,8 +206,6 @@ erDiagram sticker_category_mapper ||--o{ sticker_category : categorized_as ``` -## 커밋 컨벤션 - ## 네이밍 룰의 우선사항 **엔티티 중심의 클래스 작명**: {Entity}{Action}{Purpose}{Layer} @@ -326,12 +324,12 @@ AgreementsService - **접두사:** `dto_` - **예시:** `dto_agreement` -### DB 관련 변수 (db connection pool 등) +### DB 관련 변수 (db connection pool 등) - **접두사:** `db_` - **예시:** `db_redisQueue`, `db_dataSource` -커밋 메세지과 머지 룰 +## 커밋 메세지과 머지 룰 ### 커밋 컨벤션 diff --git a/deploy/deploy-prod.sh b/deploy/deploy-prod.sh index cc4d242..9b24063 100755 --- a/deploy/deploy-prod.sh +++ b/deploy/deploy-prod.sh @@ -2,7 +2,7 @@ # 스크립트의 실제 위치를 기준으로 경로 설정 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR=$(cd "$SCRIPT_DIR/.."; pwd) -PEM_PATH="$SCRIPT_DIR/../../../keys/blccu-prod.pem" +PEM_PATH="$SCRIPT_DIR/../../../keys/blccu-dev-rsa.pem" # PEM 파일 경로가 올바른지 확인 if [[ ! -f "$PEM_PATH" ]]; then @@ -13,12 +13,12 @@ fi # PEM 파일 권한 확인 및 수정 chmod 400 "$PEM_PATH" -HOSTS=("13.209.215.21") +HOSTS=("3.34.58.11") ACCOUNT=ubuntu -SERVICE_NAME=blccu +SERVICE_NAME=blccu-ecr DOCKER_TAG=latest -ECR_URL="792939917746.dkr.ecr.ap-northeast-2.amazonaws.com" -AWS_PROFILE=production # 배포 프로파일 사용 +ECR_URL="637423583546.dkr.ecr.ap-northeast-2.amazonaws.com" +AWS_PROFILE=staging # 배포 프로파일 사용 ENV_FILE="$ROOT_DIR/.env.prod" NGINX_CONFIG=/etc/nginx/nginx.conf @@ -66,7 +66,7 @@ for HOST in "${HOSTS[@]}"; do echo -e "\n## new docker pull & run on $HOST ##\n" ssh -i $PEM_PATH $SERVER "aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $ECR_URL" ssh -i $PEM_PATH $SERVER "docker pull $ECR_URL/$SERVICE_NAME:$DOCKER_TAG" - ssh -i $PEM_PATH $SERVER "docker run --env-file /home/$ACCOUNT/upload/.env.prod -d -p $NEW_PORT:3000 --name $NEW_SERVICE_NAME -e TZ=Asia/Seoul --network blccu_network $ECR_URL/$SERVICE_NAME" + ssh -i $PEM_PATH $SERVER "docker run --env-file /home/$ACCOUNT/upload/.env.prod -d -p $NEW_PORT:3000 --name $NEW_SERVICE_NAME -e TZ=Asia/Seoul $ECR_URL/$SERVICE_NAME" # 헬스체크 수행 echo -e "\n## 헬스체크 수행 on $HOST ##\n" diff --git a/package.json b/package.json index e881fcb..5cc9eb8 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "license": "UNLICENSED", "scripts": { "ssh:staging": "ssh -i ../../keys/blccu-dev-rsa.pem ubuntu@staging.api.blccu.com;", + "ssh:prod": "ssh -i ../../keys/blccu-dev-rsa.pem ubuntu@3.34.58.11", "ssh:prod1": "ssh -i ../../keys/blccu-prod.pem ubuntu@13.209.215.21", "ssh:prod2": "ssh -i ../../keys/blccu-prod.pem ubuntu@3.35.68.4", "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js", diff --git a/src/APIs/agreements/__test__/agreements.service.spec.ts b/src/APIs/agreements/__test__/agreements.service.spec.ts index 0768aab..1f60be3 100644 --- a/src/APIs/agreements/__test__/agreements.service.spec.ts +++ b/src/APIs/agreements/__test__/agreements.service.spec.ts @@ -54,6 +54,7 @@ describe('AgreementsService', () => { ...TEST_DATE_FIELDS, }; }); + describe('createAgreement', () => { it('should return AgreementDto with valid input', async () => { const createAgreementInput: IAgreementsServiceCreate = { diff --git a/src/APIs/announcements/__test__/announcements.controller.spec.ts b/src/APIs/announcements/__test__/announcements.controller.spec.ts new file mode 100644 index 0000000..d0c0886 --- /dev/null +++ b/src/APIs/announcements/__test__/announcements.controller.spec.ts @@ -0,0 +1,155 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AnnouncementsController } from '../announcements.controller'; +import { AnnouncementsService } from '../announcements.service'; +import { + MockService, + MockServiceFactory, + TEST_DATE_FIELDS, +} from '@/utils/test.utils'; +import { Request } from 'express'; +import { AnnouncementCreateRequestDto } from '../dtos/request/announcement-create-request.dto'; +import { AnnouncementDto } from '../dtos/common/announcement.dto'; +import { AnnouncementPatchRequestDto } from '../dtos/request/announcement-patch-request.dto'; +import { + BlccuExceptionTest, + BlccuHttpException, +} from '@/common/blccu-exception'; + +describe('announcementsController', () => { + let req: Request; + let ctrl_announcements: AnnouncementsController; + let svc_annoucements: MockService; + let dto_announcement: AnnouncementDto; + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AnnouncementsController], + providers: [ + { + provide: AnnouncementsService, + useValue: MockServiceFactory.getMockService(AnnouncementsService), + }, + ], + }).compile(); + + dto_announcement = { + id: 1, + title: '제목없음', + content: '공지내용', + ...TEST_DATE_FIELDS, + }; + ctrl_announcements = module.get( + AnnouncementsController, + ); + svc_annoucements = + module.get>(AnnouncementsService); + req = { user: { userId: 1 } } as Request; + }); + + describe('createAnnouncement', () => { + it('should return AnnouncementDto for valid input', async () => { + // Arrange + const dto_createAnnouncement: AnnouncementCreateRequestDto = { + title: '제목없음', + content: '공지내용', + }; + svc_annoucements.createAnnoucement.mockResolvedValue(dto_announcement); + // Act + const result = await ctrl_announcements.createAnnouncement( + req, + dto_createAnnouncement, + ); + // Assert + expect(result).toEqual(dto_announcement); + expect(svc_annoucements.createAnnoucement).toHaveBeenCalledWith({ + ...dto_createAnnouncement, + userId: req.user.userId, + }); + }); + }); + + describe('getAnnouncements', () => { + it('should return AnnoucementDto[] for any condition', async () => { + // Arrange + svc_annoucements.getAnnouncements.mockResolvedValue([dto_announcement]); + // Act + const result = await ctrl_announcements.getAnnouncements(); + // Assert + expect(result).toEqual([dto_announcement]); + expect(svc_annoucements.getAnnouncements).toHaveBeenCalled(); + }); + }); + + describe('patchAnnouncement', () => { + it('should return AnnouncementDto for valid input', async () => { + // Arrange + const announcementId = 1; + const dto_patchAnnouncement: AnnouncementPatchRequestDto = { + title: '수정된 제목', + content: '수정된 내용', + }; + svc_annoucements.patchAnnouncement.mockResolvedValue({ + dto_announcement, + ...dto_patchAnnouncement, + }); + + // Act + const result = await ctrl_announcements.patchAnnouncement( + req, + dto_patchAnnouncement, + announcementId, + ); + // Assert + expect(result).toEqual({ dto_announcement, ...dto_patchAnnouncement }); + expect(svc_annoucements.patchAnnouncement).toHaveBeenCalledWith({ + ...dto_patchAnnouncement, + announcementId, + userId: req.user.userId, + }); + }); + + it('should throw exception for non-admin user', async () => { + // Arrange + const announcementId = 1; + const dto_patchAnnouncement: AnnouncementPatchRequestDto = { + title: '수정된 제목', + content: '수정된 내용', + }; + svc_annoucements.patchAnnouncement.mockRejectedValue( + new BlccuHttpException('NOT_AN_ADMIN'), + ); + // Act + const act = () => + ctrl_announcements.patchAnnouncement( + req, + dto_patchAnnouncement, + announcementId, + ); + // Assert + expect(act()).rejects.toThrow(BlccuExceptionTest('NOT_AN_ADMIN')); + expect(svc_annoucements.patchAnnouncement).toHaveBeenCalledWith({ + ...dto_patchAnnouncement, + announcementId, + userId: req.user.userId, + }); + }); + }); + + describe('removeAnnouncement', () => { + it('should return AnnouncementDto for valid input', async () => { + // Arrange + const announcementId = 1; + svc_annoucements.removeAnnouncement.mockResolvedValue(dto_announcement); + // Act + const result = await ctrl_announcements.removeAnnouncement( + req, + announcementId, + ); + // Assert + expect(result).toEqual(dto_announcement); + expect(svc_annoucements.removeAnnouncement).toHaveBeenCalledWith({ + userId: req.user.userId, + announcementId, + }); + }); + }); +}); diff --git a/src/APIs/announcements/__test__/announcements.service.spec.ts b/src/APIs/announcements/__test__/announcements.service.spec.ts new file mode 100644 index 0000000..a5ff1e4 --- /dev/null +++ b/src/APIs/announcements/__test__/announcements.service.spec.ts @@ -0,0 +1,274 @@ +import { + MockRepository, + MockRepositoryFactory, + MockService, + MockServiceFactory, + TEST_DATE_FIELDS, +} from '@/utils/test.utils'; +import { AnnouncementsService } from '../announcements.service'; +import { AnnouncementsRepository } from '../announcements.repository'; +import { AnnouncementDto } from '../dtos/common/announcement.dto'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { UsersValidateService } from '@/APIs/users/services/users-validate-service'; +import { + IAnnouncementsServiceCreateAnnouncement, + IAnnouncementsServiceId, + IAnnouncementsServicePatchAnnouncement, +} from '../interfaces/announcements.service.interface'; +import { UserDto } from '@/APIs/users/dtos/common/user.dto'; +import { + BlccuExceptionTest, + BlccuHttpException, +} from '@/common/blccu-exception'; + +describe('AnnouncementsService', () => { + let svc_announcements: AnnouncementsService; + let repo_announcements: MockRepository; + let svc_usersValidate: MockService; + + let dto_announcement: AnnouncementDto; + let dto_user: UserDto; + let dto_adminUser: UserDto; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AnnouncementsService, + { + provide: getRepositoryToken(AnnouncementsRepository), + useValue: MockRepositoryFactory.getMockRepository( + AnnouncementsRepository, + ), + }, + { + provide: UsersValidateService, + useValue: MockServiceFactory.getMockService(UsersValidateService), + }, + ], + }).compile(); + + svc_announcements = module.get(AnnouncementsService); + repo_announcements = module.get>( + getRepositoryToken(AnnouncementsRepository), + ); + svc_usersValidate = + module.get>(UsersValidateService); + dto_announcement = { + id: 1, + title: '공지사항', + content: '공지내용', + ...TEST_DATE_FIELDS, + }; + dto_user = { + id: 1, + isAdmin: false, + handle: 'user', + username: '유저', + followerCount: 0, + followingCount: 0, + description: '소개', + profileImage: '', + backgroundImage: '', + ...TEST_DATE_FIELDS, + }; + dto_adminUser = { ...dto_user, isAdmin: true }; + }); + + describe('createAnnoucement', () => { + const createAnnoucementInput: IAnnouncementsServiceCreateAnnouncement = { + userId: 1, + title: '공지사항', + content: '공지내용', + }; + + it('should return AnnouncementDto with valid input', async () => { + repo_announcements.save.mockResolvedValue(dto_announcement); + svc_usersValidate.adminCheck.mockResolvedValue(dto_adminUser); + const result = await svc_announcements.createAnnoucement( + createAnnoucementInput, + ); + expect(result).toEqual(dto_announcement); + expect(repo_announcements.save).toHaveBeenCalledWith({ + title: createAnnoucementInput.title, + content: createAnnoucementInput.content, + }); + }); + + it('should throw exception for invalid userId', async () => { + repo_announcements.save.mockResolvedValue(dto_announcement); + svc_usersValidate.adminCheck.mockRejectedValue( + new BlccuHttpException('USER_NOT_FOUND'), + ); + + expect( + svc_announcements.createAnnoucement(createAnnoucementInput), + ).rejects.toThrow(BlccuExceptionTest('USER_NOT_FOUND')); + expect(repo_announcements.save).not.toHaveBeenCalled(); + }); + + it('should throw exception for non-admin user', async () => { + repo_announcements.save.mockResolvedValue(dto_announcement); + svc_usersValidate.adminCheck.mockRejectedValue( + new BlccuHttpException('NOT_AN_ADMIN'), + ); + + expect( + svc_announcements.createAnnoucement(createAnnoucementInput), + ).rejects.toThrow(BlccuExceptionTest('NOT_AN_ADMIN')); + expect(repo_announcements.save).not.toHaveBeenCalled(); + }); + }); + + describe('existCheck', () => { + it('should throw exception when announcement does not exist', async () => { + const existCheckInput: IAnnouncementsServiceId = { announcementId: 1 }; + const findOneOutput: AnnouncementDto = null; + repo_announcements.findOne.mockResolvedValue(findOneOutput); + await expect( + svc_announcements.existCheck(existCheckInput), + ).rejects.toThrow(BlccuExceptionTest('ANNOUNCEMENT_NOT_FOUND')); + expect(repo_announcements.findOne).toHaveBeenCalledWith({ + where: { id: existCheckInput.announcementId }, + }); + }); + + it('should return AnnouncementDto when announcement exists', async () => { + const existCheckInput: IAnnouncementsServiceId = { announcementId: 1 }; + repo_announcements.findOne.mockResolvedValue(dto_announcement); + expect(svc_announcements.existCheck(existCheckInput)).resolves.toEqual( + dto_announcement, + ); + expect(repo_announcements.findOne).toHaveBeenCalledWith({ + where: { id: existCheckInput.announcementId }, + }); + }); + + describe('patchAnnouncement', () => { + const patchAnnouncementInput: IAnnouncementsServicePatchAnnouncement = { + userId: 1, + announcementId: 1, + title: '수정된 제목', + content: '수정된 내용', + }; + + it('should return AnnouncementDto with valid input', async () => { + const patchAnnouncementOutput = { + ...dto_announcement, + title: patchAnnouncementInput.title, + content: patchAnnouncementInput.content, + }; + jest + .spyOn(svc_announcements, 'existCheck') + .mockResolvedValue(dto_announcement); + svc_usersValidate.adminCheck.mockResolvedValue(dto_adminUser); + + repo_announcements.save.mockResolvedValue(patchAnnouncementOutput); + repo_announcements.findOne.mockResolvedValue(patchAnnouncementOutput); + + await expect( + svc_announcements.patchAnnouncement(patchAnnouncementInput), + ).resolves.toEqual(patchAnnouncementOutput); + + expect(repo_announcements.findOne).toHaveBeenCalledWith({ + where: { id: patchAnnouncementInput.announcementId }, + }); + expect(repo_announcements.save).toHaveBeenCalledWith( + patchAnnouncementOutput, + ); + }); + + it('should throw exception for invalid userId', async () => { + const patchAnnouncementOutput = { + ...dto_announcement, + title: patchAnnouncementInput.title, + content: patchAnnouncementInput.content, + }; + repo_announcements.save.mockResolvedValue(patchAnnouncementOutput); + repo_announcements.findOne.mockResolvedValue(patchAnnouncementOutput); + svc_usersValidate.adminCheck.mockRejectedValue( + new BlccuHttpException('USER_NOT_FOUND'), + ); + jest + .spyOn(svc_announcements, 'existCheck') + .mockResolvedValue(dto_announcement); + await expect( + svc_announcements.patchAnnouncement(patchAnnouncementInput), + ).rejects.toThrow(BlccuExceptionTest('USER_NOT_FOUND')); + expect(svc_announcements.existCheck).not.toHaveBeenCalled(); + expect(repo_announcements.save).not.toHaveBeenCalled(); + expect(repo_announcements.findOne).not.toHaveBeenCalled(); + }); + + it('should throw exception for non-admin user', async () => { + const patchAnnouncementOutput = { + ...dto_announcement, + title: patchAnnouncementInput.title, + content: patchAnnouncementInput.content, + }; + repo_announcements.save.mockResolvedValue(patchAnnouncementOutput); + repo_announcements.findOne.mockResolvedValue(patchAnnouncementOutput); + svc_usersValidate.adminCheck.mockRejectedValue( + new BlccuHttpException('NOT_AN_ADMIN'), + ); + jest + .spyOn(svc_announcements, 'existCheck') + .mockResolvedValue(dto_announcement); + await expect( + svc_announcements.patchAnnouncement(patchAnnouncementInput), + ).rejects.toThrow(BlccuExceptionTest('NOT_AN_ADMIN')); + expect(svc_announcements.existCheck).not.toHaveBeenCalled(); + expect(repo_announcements.save).not.toHaveBeenCalled(); + expect(repo_announcements.findOne).not.toHaveBeenCalled(); + }); + + it('should throw exception for invalid announcementId', async () => { + const patchAnnouncementOutput = { + ...dto_announcement, + title: patchAnnouncementInput.title, + content: patchAnnouncementInput.content, + }; + repo_announcements.save.mockResolvedValue(patchAnnouncementOutput); + repo_announcements.findOne.mockResolvedValue(patchAnnouncementOutput); + svc_usersValidate.adminCheck.mockResolvedValue(dto_adminUser); + svc_announcements.existCheck = jest.fn(); + jest + .spyOn(svc_announcements, 'existCheck') + .mockRejectedValue(new BlccuHttpException('ANNOUNCEMENT_NOT_FOUND')); + + await expect( + svc_announcements.patchAnnouncement(patchAnnouncementInput), + ).rejects.toThrow(BlccuExceptionTest('ANNOUNCEMENT_NOT_FOUND')); + + expect(repo_announcements.save).not.toHaveBeenCalled(); + expect(repo_announcements.findOne).not.toHaveBeenCalled(); + }); + }); + describe('removeAnnouncement', () => { + it('should return AgreementDto with valid input', async () => { + //arrange + jest + .spyOn(svc_announcements, 'existCheck') + .mockResolvedValue(dto_announcement); + svc_usersValidate.adminCheck.mockResolvedValue(dto_adminUser); + repo_announcements.softRemove.mockResolvedValue(dto_announcement); + //act + const result = await svc_announcements.removeAnnouncement({ + userId: dto_adminUser.id, + announcementId: dto_announcement.id, + }); + //assert + expect(result).toEqual(dto_announcement); + expect(svc_usersValidate.adminCheck).toHaveBeenCalledWith({ + userId: dto_adminUser.id, + }); + expect(svc_announcements.existCheck).toHaveBeenCalledWith({ + announcementId: dto_announcement.id, + }); + expect(repo_announcements.softRemove).toHaveBeenCalledWith( + dto_announcement, + ); + }); + }); + }); +}); diff --git a/src/APIs/announcements/announcements.module.ts b/src/APIs/announcements/announcements.module.ts index a70bc06..f66742e 100644 --- a/src/APIs/announcements/announcements.module.ts +++ b/src/APIs/announcements/announcements.module.ts @@ -4,10 +4,11 @@ import { AnnouncementsService } from './announcements.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Announcement } from './entities/announcement.entity'; import { UsersModule } from '../users/users.module'; +import { AnnouncementsRepository } from './announcements.repository'; @Module({ imports: [TypeOrmModule.forFeature([Announcement]), UsersModule], controllers: [AnnouncementsController], - providers: [AnnouncementsService], + providers: [AnnouncementsService, AnnouncementsRepository], }) export class AnnouncementsModule {} diff --git a/src/APIs/announcements/announcements.repository.ts b/src/APIs/announcements/announcements.repository.ts new file mode 100644 index 0000000..14be8fe --- /dev/null +++ b/src/APIs/announcements/announcements.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { Announcement } from './entities/announcement.entity'; + +@Injectable() +export class AnnouncementsRepository extends Repository { + constructor(private db_dataSource: DataSource) { + super(Announcement, db_dataSource.createEntityManager()); + } +} diff --git a/src/APIs/announcements/announcements.service.ts b/src/APIs/announcements/announcements.service.ts index d927c63..cfe1df7 100644 --- a/src/APIs/announcements/announcements.service.ts +++ b/src/APIs/announcements/announcements.service.ts @@ -1,7 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Announcement } from './entities/announcement.entity'; -import { Repository } from 'typeorm'; import { IAnnouncementsServiceCreateAnnouncement, IAnnouncementsServiceId, @@ -13,12 +10,12 @@ import { AnnouncementDto } from './dtos/common/announcement.dto'; import { MergeExceptionMetadata } from '@/common/decorators/merge-exception-metadata.decorator'; import { BlccuException, EXCEPTIONS } from '@/common/blccu-exception'; import { ExceptionMetadata } from '@/common/decorators/exception-metadata.decorator'; +import { AnnouncementsRepository } from './announcements.repository'; @Injectable() export class AnnouncementsService { constructor( - @InjectRepository(Announcement) - private readonly repo_announcements: Repository, + private readonly repo_announcements: AnnouncementsRepository, private readonly svc_usersValidate: UsersValidateService, ) {} diff --git a/src/APIs/articleBackgrounds/articleBackgrounds.service.ts b/src/APIs/articleBackgrounds/articleBackgrounds.service.ts index 96d81ec..17f9017 100644 --- a/src/APIs/articleBackgrounds/articleBackgrounds.service.ts +++ b/src/APIs/articleBackgrounds/articleBackgrounds.service.ts @@ -30,6 +30,7 @@ export class ArticleBackgroundsService { file, resize: 2000, ext: 'png', + tag: 'articles/images/backgrounds', }); await this.repo_articleBackgrounds.save({ imageUrl }); return { imageUrl }; diff --git a/src/APIs/articles/services/articles-create.service.ts b/src/APIs/articles/services/articles-create.service.ts index 11a9e11..821ab10 100644 --- a/src/APIs/articles/services/articles-create.service.ts +++ b/src/APIs/articles/services/articles-create.service.ts @@ -133,6 +133,7 @@ export class ArticlesCreateService { file, ext: 'png', resize: 1280, + tag: 'articles/images/views', }); return { imageUrl }; diff --git a/src/APIs/stickers/stickers.service.ts b/src/APIs/stickers/stickers.service.ts index 465e5a1..30e76af 100644 --- a/src/APIs/stickers/stickers.service.ts +++ b/src/APIs/stickers/stickers.service.ts @@ -49,6 +49,7 @@ export class StickersService { file, resize: 1600, ext: 'png', + tag: 'stickers/images', }); const insertData = await this.repo_stickers .createQueryBuilder() diff --git a/src/APIs/users/services/users-update.service.ts b/src/APIs/users/services/users-update.service.ts index 01c2dc1..900bd28 100644 --- a/src/APIs/users/services/users-update.service.ts +++ b/src/APIs/users/services/users-update.service.ts @@ -76,6 +76,7 @@ export class UsersUpdateService { ext: 'jpg', file, resize: 800, + tag: 'users/images/profiles', }); await this.svc_images.deleteImage({ url: user.profileImage }); @@ -99,6 +100,7 @@ export class UsersUpdateService { ext: 'jpg', file, resize: 1600, + tag: 'users/images/backgrounds', }); await this.svc_images.deleteImage({ url: user.backgroundImage }); diff --git a/src/modules/aws/aws.service.ts b/src/modules/aws/aws.service.ts index e235cf2..5ad0a15 100644 --- a/src/modules/aws/aws.service.ts +++ b/src/modules/aws/aws.service.ts @@ -72,10 +72,11 @@ export class AwsService { ) { try { const resizedImageBuffer = await this.resizeImage(file.buffer, resize); + const key = `${this.configService.get('S3_TAG')}/${fileName}`; // AWS S3에 이미지 업로드 명령을 생성합니다. 파일 이름, 파일 버퍼, 파일 접근 권한, 파일 타입 등을 설정합니다. const command = new PutObjectCommand({ Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), // S3 버킷 이름 - Key: fileName, // 업로드될 파일의 이름 + Key: key, // 업로드될 파일의 이름 Body: resizedImageBuffer, // 업로드할 파일 ContentType: `image/${ext}`, // 파일 타입 }); @@ -84,7 +85,7 @@ export class AwsService { await this.s3Client.send(command); // 업로드된 이미지의 URL을 반환합니다. - return `https://${this.configService.get('CLOUDFRONT_DOMAIN_NAME')}/${fileName}`; + return `https://${this.configService.get('CLOUDFRONT_DOMAIN_NAME')}/${key}`; } catch (error) { this.logger.error('Error uploading image to S3', error.stack); throw new BlccuException('IMAGE_UPLOAD_TO_S3_ERROR'); diff --git a/src/modules/images/images.service.ts b/src/modules/images/images.service.ts index a1b226a..bde3d4e 100644 --- a/src/modules/images/images.service.ts +++ b/src/modules/images/images.service.ts @@ -19,10 +19,11 @@ export class ImagesService { file, resize, ext, + tag, }: IImagesServiceUploadImage): Promise { const imageName = getUUID(); const imageUrl = await this.svc_aws.imageUploadToS3( - `${imageName}.${ext}`, + `${tag}/${imageName}.${ext}`, file, ext, resize, diff --git a/src/modules/images/interfaces/images.service.interface.ts b/src/modules/images/interfaces/images.service.interface.ts index 0124550..249f029 100644 --- a/src/modules/images/interfaces/images.service.interface.ts +++ b/src/modules/images/interfaces/images.service.interface.ts @@ -2,6 +2,7 @@ export interface IImagesServiceUploadImage { file: Express.Multer.File; resize: number; ext: 'jpg' | 'png'; + tag?: string; } export interface IImagesServiceDeleteImage {