Skip to content

Commit 59d211c

Browse files
authored
관리자가 업로드한 음악 파일 m3u8로 형식 파싱하고 Object storage에 업로드, 이미지 Object storage에 업로드 (#77)
* fix: client workspace 인식 문제 해결 * fix: redis container 삭제 * fix: server package에서 모듈 타입 삭제 * feat: local에서 도커 개발환경 구축 * feat: AdminController 전반적인 구성 및 사용 데이터타입 지정 * feat: 임시 디렉토리 설정 및 form-data를 이용한 파일 받아오기 기능 구현 * feat: 관리자가 업데이트한 여러 음악파일을 파싱한 후 object storage에 저장 할 수 있는 기능 구현 * fix: Docker 로컬 환경으로 수정 * fix: default.conf에서의 머지 컨플릭트 해결 * fix: dev 형식으로 수정 * refactor: 새로운 클라이언트 입력 형식에 따른 파일 파싱 로직 수정 * fix: datatype 더 확실히 명시 * update: 현재까지 최신 구현 * fix: 배포 환경으로 수정
1 parent a5da66f commit 59d211c

16 files changed

+357
-26
lines changed

docker-compose.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ services:
2525
networks:
2626
- webapp
2727
healthcheck:
28-
test: ["CMD-SHELL", "wget -q --spider http://server:3000/api/health || exit 1"]
28+
test:
29+
[
30+
'CMD-SHELL',
31+
'wget -q --spider http://server:3000/api/health || exit 1',
32+
]
2933
interval: 7s
3034
timeout: 10s
3135
retries: 5
@@ -57,6 +61,7 @@ services:
5761
container_name: nginx
5862
volumes:
5963
- ./nginx/conf.d:/etc/nginx/conf.d:ro
64+
- ./client/dist/:/usr/share/nginx/html
6065
ports:
6166
- '80:80'
6267
depends_on:

nginx/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
FROM nginx:alpine
22

3-
COPY nginx/nginx.conf /etc/nginx/nginx.conf
3+
COPY nginx/nginx.conf /etc/nginx/nginx.conf

nginx/conf.d/default.conf

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ server {
2525
location /socket.io {
2626
proxy_pass http://backend;
2727
}
28-
}
28+
}

server/Dockerfile

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ FROM node:22-alpine
22

33
WORKDIR /app
44

5+
RUN apk add --no-cache ffmpeg
6+
57
COPY package.json yarn.lock .yarnrc.yml tsconfig.json ./
68
COPY .yarn .yarn
79
COPY server/ server/

server/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
"@types/socket.io": "^3.0.2",
3939
"aws-sdk": "^2.1692.0",
4040
"cache-manager": "5.2.3",
41+
"class-transformer": "^0.5.1",
42+
"class-validator": "^0.14.1",
4143
"express": "^4.21.1",
4244
"fluent-ffmpeg": "^2.1.3",
4345
"hls-server": "^1.5.0",

server/src/admin/admin.controller.ts

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {
2+
Body,
3+
Controller,
4+
Inject,
5+
Post,
6+
UploadedFiles,
7+
UseInterceptors,
8+
} from '@nestjs/common';
9+
import { REDIS_CLIENT } from '@/common/redis/redis.module';
10+
import { RedisClientType } from 'redis';
11+
import { AlbumDto } from './dto/AlbumDto';
12+
import { FileFieldsInterceptor } from '@nestjs/platform-express';
13+
import path from 'path';
14+
import * as fs from 'fs/promises';
15+
import { MusicProcessingSevice } from '@/music/music.processor';
16+
import { AdminService } from './admin.service';
17+
18+
export interface UploadedFiles {
19+
albumCover?: Express.Multer.File;
20+
bannerCover?: Express.Multer.File;
21+
songs: Express.Multer.File[];
22+
}
23+
24+
@Controller('admin')
25+
export class AdminController {
26+
constructor(
27+
@Inject(REDIS_CLIENT) private readonly redisClient: RedisClientType,
28+
@Inject() private readonly musicProcessingService: MusicProcessingSevice,
29+
private readonly adminService: AdminService,
30+
) {}
31+
32+
@Post('album')
33+
@UseInterceptors(
34+
FileFieldsInterceptor([
35+
{ name: 'albumCover' },
36+
{ name: 'bannerCover' },
37+
{ name: 'songs' },
38+
]),
39+
)
40+
async createAlbum(
41+
@UploadedFiles() files: UploadedFiles,
42+
@Body('albumData') albumDataString: string,
43+
): Promise<any> {
44+
const albumData = JSON.parse(albumDataString) as AlbumDto;
45+
// TODO 성준님 1. MySQL DB에 앨범 정보 저장 하고 앨범 Id를 반환 받음
46+
//const albumId = this.adminRepository.createAlbum();
47+
48+
// 앨범 임시 ID
49+
const albumId = 'RANDOM_AHH_ALBUM_ID';
50+
const imageUrls: {
51+
albumCoverURL?: string;
52+
bannerCoverURL?: string;
53+
} = {};
54+
55+
//2. 앨범 커버 이미지, 배너 이미지를 S3에 업로드 하고 URL을 반환 받음
56+
if (files.albumCover?.[0] || files.bannerCover?.[0]) {
57+
const uploadResults = await this.adminService.uploadImageFiles(
58+
files.albumCover?.[0],
59+
files.bannerCover?.[0],
60+
`converted/${albumId}`,
61+
);
62+
63+
Object.assign(imageUrls, uploadResults);
64+
}
65+
66+
// await this.adminRepository.updateAlbumUrls(albumId, {
67+
// albumCoverURL,
68+
// bannerCoverURL
69+
// });
70+
71+
//3. 노래 파일들 처리: 기존 processSongFiles 사용
72+
const message = await this.processSongFiles(
73+
files.songs,
74+
albumData,
75+
albumId,
76+
);
77+
return message;
78+
79+
// 4. MySQL DB에 노래 정보 저장
80+
//return { albumId };
81+
}
82+
83+
private async processSongFiles(
84+
songFiles: Express.Multer.File[],
85+
albumData: AlbumDto,
86+
albumId: string,
87+
): Promise<any> {
88+
const tempDir = await this.createTempDirectory(albumId);
89+
90+
await Promise.all(
91+
songFiles.map(async (file, index) => {
92+
const songInfo = albumData.songs[index];
93+
await this.musicProcessingService.processUpload(file, tempDir, {
94+
albumId,
95+
...songInfo,
96+
});
97+
}),
98+
);
99+
100+
await fs.rm(tempDir, { recursive: true, force: true });
101+
102+
return {
103+
albumId,
104+
message: 'Album songs updated to object storage successfully',
105+
};
106+
}
107+
108+
private async createTempDirectory(albumId: string): Promise<string> {
109+
const tempDir = path.join(__dirname, `album/${albumId}`);
110+
await fs.mkdir(tempDir, { recursive: true });
111+
return tempDir;
112+
}
113+
}

server/src/admin/admin.module.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Module } from '@nestjs/common';
2+
import { ConfigModule } from '@nestjs/config';
3+
import { AdminController } from './admin.controller';
4+
import { MusicModule } from '@/music/music.module';
5+
import { AdminService } from './admin.service';
6+
7+
@Module({
8+
imports: [
9+
ConfigModule.forRoot({
10+
isGlobal: true,
11+
envFilePath: '.env',
12+
}),
13+
MusicModule,
14+
],
15+
controllers: [AdminController],
16+
providers: [AdminService],
17+
exports: [AdminService],
18+
})
19+
export class AdminModule {}

server/src/admin/admin.service.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import * as AWS from 'aws-sdk';
4+
5+
@Injectable()
6+
export class AdminService {
7+
private s3;
8+
9+
constructor(private configService: ConfigService) {
10+
this.s3 = new AWS.S3({
11+
endpoint: new AWS.Endpoint('https://kr.object.ncloudstorage.com'),
12+
region: 'kr-standard',
13+
credentials: {
14+
accessKeyId: this.configService.get<string>('S3_ACCESS_KEY'),
15+
secretAccessKey: this.configService.get<string>('S3_SECRET_KEY'),
16+
},
17+
});
18+
}
19+
20+
async uploadImageFiles(
21+
albumCoverFile: Express.Multer.File,
22+
bannerCoverFile: Express.Multer.File,
23+
prefix: string,
24+
) {
25+
const results: {
26+
albumCoverURL?: string;
27+
bannerCoverURL?: string;
28+
} = {};
29+
30+
if (albumCoverFile) {
31+
results.albumCoverURL = await this.uploadFile(
32+
albumCoverFile,
33+
prefix,
34+
'cover',
35+
);
36+
}
37+
38+
if (bannerCoverFile) {
39+
results.bannerCoverURL = await this.uploadFile(
40+
bannerCoverFile,
41+
prefix,
42+
'banner',
43+
);
44+
}
45+
46+
return results;
47+
}
48+
49+
async uploadFile(
50+
file: Express.Multer.File,
51+
folder: string,
52+
fileType: 'cover' | 'banner',
53+
): Promise<string> {
54+
const bucket = this.configService.get<string>('S3_BUCKET_NAME');
55+
const fileExtension = file.originalname.split('.').pop();
56+
const key = `${folder}/${fileType}.${fileExtension}`;
57+
58+
const uploadParams = {
59+
Bucket: bucket,
60+
Key: key,
61+
Body: file.buffer,
62+
ContentType: file.mimetype,
63+
ACL: 'public-read',
64+
};
65+
66+
try {
67+
const result = await this.s3.upload(uploadParams).promise();
68+
return result.Location;
69+
} catch (error) {
70+
throw new Error(`Failed to upload file to S3: ${error.message}`);
71+
}
72+
}
73+
}

server/src/admin/dto/AlbumDto.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {
2+
IsArray,
3+
IsDateString,
4+
IsNotEmpty,
5+
IsNumber,
6+
IsString,
7+
Min,
8+
} from 'class-validator';
9+
import { SongDto } from './SongDto';
10+
11+
export class AlbumDto {
12+
@IsNotEmpty()
13+
@IsString()
14+
title: string;
15+
16+
@IsNotEmpty()
17+
@IsString()
18+
artist: string;
19+
20+
@IsNotEmpty()
21+
@IsDateString()
22+
releaseDate: string;
23+
24+
@IsString()
25+
releaseTime?: string;
26+
27+
@IsNotEmpty()
28+
@IsNumber()
29+
@Min(0)
30+
totalTracks?: number;
31+
32+
@IsArray()
33+
songs: SongDto[];
34+
}

server/src/admin/dto/SongDto.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {
2+
IsArray,
3+
IsDateString,
4+
IsNotEmpty,
5+
IsNumber,
6+
IsString,
7+
Min,
8+
} from 'class-validator';
9+
10+
export class SongDto {
11+
@IsString()
12+
@IsNotEmpty()
13+
title: string;
14+
15+
@IsNumber()
16+
@Min(1)
17+
trackNumber: number;
18+
19+
@IsString()
20+
lyrics: string;
21+
22+
@IsString()
23+
composer: string;
24+
25+
@IsString()
26+
writer: string;
27+
28+
@IsString()
29+
instrument: string;
30+
}

server/src/album/album.service.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

server/src/app.module.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@ import { RoomController } from '@/room/room.controller';
99
import { RoomGateway } from './room/room.gateway';
1010
import { MusicProcessingSevice } from './music/music.processor';
1111
import { MusicModule } from './music/music.module';
12+
import { AdminModule } from './admin/admin.module';
1213

1314
@Module({
14-
imports: [CommonModule, ConfigModule.forRoot(), RedisModule, MusicModule],
15+
imports: [
16+
CommonModule,
17+
ConfigModule.forRoot(),
18+
RedisModule,
19+
MusicModule,
20+
AdminModule,
21+
],
1522
controllers: [AppController, RoomController],
1623
providers: [Logger, AppService, RoomRepository, RoomGateway],
1724
})

server/src/main.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ async function bootstrap() {
1919
.setVersion('1.0.0')
2020
.build();
2121
const document = SwaggerModule.createDocument(app, config);
22-
SwaggerModule.setup('api', app, document);
23-
24-
app.useStaticAssets(join(__dirname, '..', 'public'));
22+
SwaggerModule.setup('api/api-document', app, document);
2523

2624
await app.listen(process.env.PORT ?? 3000);
2725
}

0 commit comments

Comments
 (0)