Skip to content

Commit 1d1fd84

Browse files
authored
Merge pull request #6 from oodd-team/OD-39
게시글 생성 기능
2 parents 05892d1 + 5211d5f commit 1d1fd84

16 files changed

+462
-32
lines changed

oodd.erd.json

+9-9
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
"settings": {
55
"width": 5000,
66
"height": 5000,
7-
"scrollTop": -350.7988,
8-
"scrollLeft": -743,
9-
"zoomLevel": 0.91,
7+
"scrollTop": -141.7535,
8+
"scrollLeft": -27.0249,
9+
"zoomLevel": 0.94,
1010
"show": 511,
1111
"database": 4,
1212
"databaseName": "oodd_dev",
@@ -359,7 +359,7 @@
359359
"color": ""
360360
},
361361
"meta": {
362-
"updateAt": 1728980695840,
362+
"updateAt": 1728840014697,
363363
"createAt": 1725423390546
364364
}
365365
},
@@ -538,15 +538,15 @@
538538
"gLA60xcIc0oDGJRREWzLC"
539539
],
540540
"ui": {
541-
"x": 682.5433,
542-
"y": 355.1232,
541+
"x": 681.4444,
542+
"y": 358.9248,
543543
"zIndex": 2,
544544
"widthName": 60,
545545
"widthComment": 60,
546546
"color": ""
547547
},
548548
"meta": {
549-
"updateAt": 1729093078702,
549+
"updateAt": 1728754878357,
550550
"createAt": 1725423390547
551551
}
552552
},
@@ -615,14 +615,14 @@
615615
],
616616
"ui": {
617617
"x": 48.8889,
618-
"y": 50,
618+
"y": 49,
619619
"zIndex": 2,
620620
"widthName": 60,
621621
"widthComment": 60,
622622
"color": ""
623623
},
624624
"meta": {
625-
"updateAt": 1725423900406,
625+
"updateAt": 1728755232306,
626626
"createAt": 1725423390548
627627
}
628628
},

src/clothing/clothing.module.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
import { Clothing } from 'src/common/entities/clothing.entity';
4+
import { ClothingService } from './clothing.service';
25

3-
@Module({})
6+
@Module({
7+
imports: [TypeOrmModule.forFeature([Clothing])],
8+
providers: [ClothingService],
9+
exports: [ClothingService],
10+
})
411
export class ClothingModule {}

src/clothing/clothing.service.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { QueryRunner, Repository } from 'typeorm';
4+
import { Clothing } from 'src/common/entities/clothing.entity';
5+
import { UploadClothingDto } from 'src/post/dtos/create-post.dto';
6+
7+
@Injectable()
8+
export class ClothingService {
9+
constructor(
10+
@InjectRepository(Clothing)
11+
private readonly clothingRepository: Repository<Clothing>,
12+
) {}
13+
14+
// Clothing 엔티티 저장
15+
async saveClothings(
16+
uploadClothingDtos: UploadClothingDto[],
17+
queryRunner: QueryRunner,
18+
): Promise<Clothing[]> {
19+
const clothingEntities = uploadClothingDtos.map(
20+
(clothing: UploadClothingDto) =>
21+
this.clothingRepository.create({
22+
imageUrl: clothing.imageUrl,
23+
brandName: clothing.brandName,
24+
modelName: clothing.modelName,
25+
modelNumber: clothing.modelNumber,
26+
url: clothing.url,
27+
}),
28+
);
29+
30+
return await queryRunner.manager.save(clothingEntities);
31+
}
32+
}
+6-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { Module } from '@nestjs/common';
22
import { PostClothingController } from './post-clothing.controller';
33
import { PostClothingService } from './post-clothing.service';
4+
import { PostClothing } from 'src/common/entities/post-clothing.entity';
5+
import { TypeOrmModule } from '@nestjs/typeorm';
6+
import { ClothingModule } from 'src/clothing/clothing.module';
47

58
@Module({
9+
imports: [TypeOrmModule.forFeature([PostClothing]), ClothingModule],
610
controllers: [PostClothingController],
7-
providers: [PostClothingService]
11+
providers: [PostClothingService],
12+
exports: [PostClothingService],
813
})
914
export class PostClothingModule {}
+34-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,37 @@
11
import { Injectable } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { ClothingService } from 'src/clothing/clothing.service';
4+
import { Clothing } from 'src/common/entities/clothing.entity';
5+
import { PostClothing } from 'src/common/entities/post-clothing.entity';
6+
import { Post } from 'src/common/entities/post.entity';
7+
import { UploadClothingDto } from 'src/post/dtos/create-post.dto';
8+
import { QueryRunner, Repository } from 'typeorm';
29

310
@Injectable()
4-
export class PostClothingService {}
11+
export class PostClothingService {
12+
constructor(
13+
@InjectRepository(PostClothing)
14+
private readonly postClothingRepository: Repository<PostClothing>,
15+
private readonly clothingService: ClothingService,
16+
) {}
17+
18+
async savePostClothings(
19+
post: Post,
20+
uploadClothingDtos: UploadClothingDto[],
21+
queryRunner?: QueryRunner,
22+
): Promise<void> {
23+
const savedClothings = await this.clothingService.saveClothings(
24+
uploadClothingDtos,
25+
queryRunner,
26+
);
27+
28+
const postClothingEntities = savedClothings.map((clothing: Clothing) =>
29+
this.postClothingRepository.create({
30+
post,
31+
clothing,
32+
}),
33+
);
34+
35+
await queryRunner.manager.save(postClothingEntities);
36+
}
37+
}

src/post-image/post-image.service.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
11
import { Injectable } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { PostImage } from 'src/common/entities/post-image.entity';
4+
import { Repository, QueryRunner } from 'typeorm';
5+
import { Post } from 'src/common/entities/post.entity';
6+
import { UploadImageDto } from 'src/post/dtos/create-post.dto';
7+
import { InvalidInputValueException } from 'src/common/exception/service.exception';
8+
29
@Injectable()
3-
export class PostImageService {}
10+
export class PostImageService {
11+
constructor(
12+
@InjectRepository(PostImage)
13+
private readonly postImageRepository: Repository<PostImage>,
14+
) {}
15+
16+
async savePostImages(
17+
postImages: UploadImageDto[],
18+
post: Post,
19+
queryRunner?: QueryRunner,
20+
) {
21+
// 빈 배열이 들어온 경우
22+
if (postImages.length === 0) {
23+
throw InvalidInputValueException('하나 이상의 이미지를 업로드하세요.');
24+
}
25+
26+
const postImageEntities = postImages.map((image: UploadImageDto) => {
27+
return this.postImageRepository.create({
28+
url: image.imageurl,
29+
orderNum: image.orderNum,
30+
post: post,
31+
});
32+
});
33+
await queryRunner.manager.save(postImageEntities);
34+
}
35+
}
+7-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import { Module } from '@nestjs/common';
22
import { PostStyletagController } from './post-styletag.controller';
33
import { PostStyletagService } from './post-styletag.service';
4+
import { TypeOrmModule } from '@nestjs/typeorm';
5+
import { PostStyletag } from 'src/common/entities/post-styletag.entity';
6+
import { Styletag } from 'src/common/entities/styletag.entity';
7+
import { StyletagService } from 'src/styletag/styletag.service';
48

59
@Module({
10+
imports: [TypeOrmModule.forFeature([PostStyletag, Styletag])],
611
controllers: [PostStyletagController],
7-
providers: [PostStyletagService]
12+
providers: [PostStyletagService, StyletagService],
13+
exports: [PostStyletagService],
814
})
915
export class PostStyletagModule {}
+58-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,61 @@
11
import { Injectable } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { QueryRunner, Repository } from 'typeorm';
4+
import { PostStyletag } from 'src/common/entities/post-styletag.entity';
5+
import { Post } from 'src/common/entities/post.entity';
6+
import { StyletagService } from 'src/styletag/styletag.service';
7+
import {
8+
DataNotFoundException,
9+
InternalServerException,
10+
InvalidInputValueException,
11+
} from 'src/common/exception/service.exception';
212

313
@Injectable()
4-
export class PostStyletagService {}
14+
export class PostStyletagService {
15+
constructor(
16+
@InjectRepository(PostStyletag)
17+
private readonly postStyletagRepository: Repository<PostStyletag>,
18+
private readonly styletagService: StyletagService,
19+
) {}
20+
21+
async savePostStyletags(
22+
post: Post,
23+
tags: string[],
24+
queryRunner: QueryRunner,
25+
): Promise<void> {
26+
if (!tags || tags.length === 0) {
27+
return;
28+
}
29+
30+
// Styletag 조회
31+
const styleTags = await this.styletagService.findStyleTags(tags);
32+
33+
if (styleTags.length === 0) {
34+
throw DataNotFoundException('일치하는 스타일 태그가 없습니다.');
35+
}
36+
37+
for (const tag of styleTags) {
38+
// 중복 검사
39+
const existingPostStyletag = await this.postStyletagRepository.findOne({
40+
where: { post, styletag: tag },
41+
});
42+
43+
if (existingPostStyletag) {
44+
throw InvalidInputValueException(`중복된 스타일 태그: ${tag.tag}`);
45+
}
46+
}
47+
48+
for (const tag of styleTags) {
49+
const postStyletag = this.postStyletagRepository.create({
50+
post,
51+
styletag: tag,
52+
});
53+
54+
try {
55+
await queryRunner.manager.save(postStyletag);
56+
} catch (error) {
57+
throw InternalServerException('postStyletag 저장에 실패했습니다.');
58+
}
59+
}
60+
}
61+
}

src/post/dtos/create-post.dto.ts

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import {
3+
IsArray,
4+
IsBoolean,
5+
IsOptional,
6+
IsString,
7+
IsNumber,
8+
ValidateNested,
9+
MaxLength,
10+
} from 'class-validator';
11+
import { Type } from 'class-transformer';
12+
13+
export class UploadImageDto {
14+
@ApiProperty({
15+
example: 'http://example.com/image.jpg',
16+
description: '업로드할 이미지의 URL입니다.',
17+
})
18+
@IsString()
19+
imageurl: string;
20+
21+
@ApiProperty({ example: 1, description: '이미지의 순서 번호입니다.' })
22+
@IsNumber()
23+
orderNum: number;
24+
}
25+
26+
export class UploadClothingDto {
27+
@ApiProperty({
28+
example: 'http://example.com/clothing.jpg',
29+
description: '업로드할 옷 정보 URL입니다.',
30+
})
31+
@IsString()
32+
imageUrl: string;
33+
34+
@ApiProperty({
35+
example: '브랜드명',
36+
description: '옷 브랜드명입니다.',
37+
})
38+
@IsString()
39+
@MaxLength(100)
40+
brandName: string;
41+
42+
@ApiProperty({ example: '모델명', description: '옷 상품명입니다.' })
43+
@IsString()
44+
@MaxLength(100)
45+
modelName: string;
46+
47+
@ApiProperty({ example: '모델 넘버', description: '옷 모델 넘버입니다.' })
48+
@IsString()
49+
@MaxLength(100)
50+
modelNumber: string;
51+
52+
@ApiProperty({
53+
example: 'http://example.com/product',
54+
description: '옷 상품 링크입니다.',
55+
})
56+
@IsString()
57+
url: string;
58+
}
59+
60+
export class CreatePostDto {
61+
@ApiProperty({
62+
example: '게시물 내용',
63+
description: '게시물 내용입니다. 최대 100자까지 입력할 수 있습니다.',
64+
})
65+
@IsString()
66+
@MaxLength(100)
67+
content: string;
68+
69+
@ApiProperty({
70+
type: [UploadImageDto],
71+
description: '게시물에 포함될 이미지 목록입니다.',
72+
})
73+
@IsOptional()
74+
@IsArray()
75+
@ValidateNested({ each: true })
76+
@Type(() => UploadImageDto)
77+
postImages?: UploadImageDto[];
78+
79+
@ApiProperty({
80+
required: false,
81+
type: [String],
82+
example: ['가을'],
83+
description:
84+
'게시글에 포함될 스타일 태그 목록입니다. 스타일 태그에 저장된 태그만 입력 가능합니다.',
85+
})
86+
@IsOptional()
87+
@IsArray()
88+
@MaxLength(20, { each: true })
89+
postStyletags?: string[];
90+
91+
@ApiProperty({
92+
required: false,
93+
type: [UploadClothingDto],
94+
description: '게시물에 포함될 옷 정보 리스트입니다.',
95+
})
96+
@IsOptional()
97+
@IsArray()
98+
@ValidateNested({ each: true })
99+
@Type(() => UploadClothingDto)
100+
postClothings?: UploadClothingDto[];
101+
102+
@ApiProperty({
103+
example: false,
104+
description: '대표 게시물 여부입니다.',
105+
})
106+
@IsBoolean()
107+
isRepresentative: boolean;
108+
}

0 commit comments

Comments
 (0)