Skip to content

Commit eff044d

Browse files
authored
Merge pull request #403 from Quickchive/fix/remove-category-content-duplication-check
fix: 카테고리 중복검사 로직 수정
2 parents 5db172d + e54b896 commit eff044d

File tree

5 files changed

+227
-36
lines changed

5 files changed

+227
-36
lines changed

src/categories/category.repository.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,23 @@ export class CategoryRepository extends Repository<Category> {
146146
},
147147
});
148148
}
149+
150+
async findByParentId(
151+
parentId: number,
152+
entityManager?: EntityManager,
153+
): Promise<Category[]> {
154+
if (entityManager) {
155+
return entityManager.find(Category, {
156+
where: {
157+
parentId,
158+
},
159+
});
160+
}
161+
162+
return await this.find({
163+
where: {
164+
parentId,
165+
},
166+
});
167+
}
149168
}

src/contents/contents.controller.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ import {
22
Body,
33
Controller,
44
Delete,
5-
Post,
5+
Get,
66
Param,
7-
UseGuards,
87
ParseIntPipe,
98
Patch,
10-
Get,
11-
UseInterceptors,
9+
Post,
1210
Query,
11+
UseGuards,
1312
} from '@nestjs/common';
1413
import {
1514
ApiBadRequestResponse,
@@ -24,10 +23,7 @@ import {
2423
} from '@nestjs/swagger';
2524
import { AuthUser } from '../auth/auth-user.decorator';
2625
import { JwtAuthGuard } from '../auth/jwt/jwt.guard';
27-
import { TransactionInterceptor } from '../common/interceptors/transaction.interceptor';
28-
import { TransactionManager } from '../common/transaction.decorator';
2926
import { User } from '../users/entities/user.entity';
30-
import { EntityManager } from 'typeorm';
3127
import { ContentsService } from './contents.service';
3228
import {
3329
AddContentBodyDto,
@@ -38,6 +34,7 @@ import {
3834
toggleFavoriteOutput,
3935
UpdateContentBodyDto,
4036
UpdateContentOutput,
37+
UpdateContentRequest,
4138
} from './dtos/content.dto';
4239
import { ErrorOutput } from '../common/dtos/output.dto';
4340
import {
@@ -118,6 +115,34 @@ export class ContentsController {
118115
return this.contentsService.updateContent(user, content);
119116
}
120117

118+
@ApiOperation({
119+
summary: '콘텐츠 정보 수정',
120+
description: '콘텐츠을 수정하는 메서드',
121+
})
122+
@ApiCreatedResponse({
123+
description: '콘텐츠 수정 성공 여부를 반환한다.',
124+
type: UpdateContentOutput,
125+
})
126+
@ApiConflictResponse({
127+
description: '동일한 링크의 콘텐츠가 같은 카테고리 내에 존재할 경우',
128+
type: ErrorOutput,
129+
})
130+
@ApiNotFoundResponse({
131+
description: '존재하지 않는 콘텐츠 또는 유저인 경우',
132+
type: ErrorOutput,
133+
})
134+
@Patch(':contentId')
135+
async updateContentV2(
136+
@AuthUser() user: User,
137+
@Body() content: UpdateContentRequest,
138+
@Param('contentId') contentId: number,
139+
): Promise<UpdateContentOutput> {
140+
return this.contentsService.updateContent(user, {
141+
...content,
142+
id: contentId,
143+
});
144+
}
145+
121146
@ApiOperation({
122147
summary: '즐겨찾기 등록 및 해제',
123148
description: '즐겨찾기에 등록 및 해제하는 메서드',

src/contents/contents.service.ts

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {
22
BadRequestException,
3+
ConflictException,
34
ForbiddenException,
45
Injectable,
56
NotFoundException,
67
} from '@nestjs/common';
7-
import { DataSource, EntityManager } from 'typeorm';
8+
import { DataSource, EntityManager, In, Not } from 'typeorm';
89

910
import {
1011
AddContentBodyDto,
@@ -184,8 +185,6 @@ export class ContentsService {
184185
reminder,
185186
favorite,
186187
categoryId,
187-
categoryName,
188-
parentId,
189188
}: UpdateContentBodyDto,
190189
entityManager?: EntityManager,
191190
): Promise<AddContentOutput> {
@@ -197,32 +196,41 @@ export class ContentsService {
197196
reminder,
198197
favorite,
199198
};
200-
const userInDb = await this.userRepository.findOneWithContentsAndCategories(
201-
user.id,
202-
);
203-
if (!userInDb) {
204-
throw new NotFoundException('User not found');
205-
}
206199

207-
const content = userInDb?.contents?.filter(
208-
(content) => content.id === contentId,
209-
)[0];
200+
const content = await this.contentRepository.findOne({
201+
where: {
202+
id: contentId,
203+
},
204+
relations: ['category'],
205+
});
206+
210207
if (!content) {
211-
throw new NotFoundException('Content not found.');
208+
throw new NotFoundException('컨텐츠가 존재하지 않습니다.');
212209
}
213210

214-
if (categoryId !== undefined) {
215-
const category =
216-
categoryId !== null
217-
? await this.categoryRepository.findById(categoryId, entityManager)
218-
: null;
211+
// 카테고리 변경이 발생하는 경우
212+
if (categoryId && !content.isSameCategory(categoryId)) {
213+
const [category, subCategories] = await Promise.all([
214+
(async () => {
215+
const category = await this.categoryRepository.findById(categoryId);
219216

220-
if (category) {
221-
await checkContentDuplicateAndAddCategorySaveLog(
222-
link,
223-
category,
224-
userInDb,
225-
);
217+
if (!category) {
218+
throw new NotFoundException('카테고리가 존재하지 않습니다.');
219+
}
220+
221+
return category;
222+
})(),
223+
this.categoryRepository.findByParentId(categoryId),
224+
]);
225+
226+
const isDuplicated = await this.isDuplicatedContents(
227+
content.id,
228+
[category, ...subCategories],
229+
content.link,
230+
);
231+
232+
if (isDuplicated) {
233+
throw new ConflictException('이미 저장된 컨텐츠입니다.');
226234
}
227235

228236
await this.contentRepository.updateOne(
@@ -471,4 +479,22 @@ export class ContentsService {
471479
throw e;
472480
}
473481
}
482+
483+
private async isDuplicatedContents(
484+
id: number,
485+
categories: Category[],
486+
link: string,
487+
) {
488+
const existingContents = await this.contentRepository.find({
489+
where: {
490+
id: Not(id),
491+
category: {
492+
id: In(categories.map((category) => category.id)),
493+
},
494+
link,
495+
},
496+
});
497+
498+
return existingContents.length > 0;
499+
}
474500
}

src/contents/dtos/content.dto.ts

Lines changed: 122 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
ApiProperty,
33
ApiPropertyOptional,
4-
IntersectionType,
54
PartialType,
65
PickType,
76
} from '@nestjs/swagger';
@@ -121,15 +120,133 @@ export class AddMultipleContentsBodyDto {
121120
}
122121

123122
class ContentBody extends PartialType(AddContentBodyDto) {}
123+
124124
class ContentIdAndDescription extends PickType(Content, [
125125
'id',
126126
'description',
127127
]) {}
128128

129-
export class UpdateContentBodyDto extends IntersectionType(
130-
ContentIdAndDescription,
131-
ContentBody,
132-
) {}
129+
export class UpdateContentBodyDto {
130+
@ApiProperty({
131+
description: '컨텐츠 id',
132+
})
133+
@IsInt()
134+
@IsPositive()
135+
id: number;
136+
137+
@ApiPropertyOptional({
138+
description: '컨텐츠 설명',
139+
})
140+
@IsString()
141+
@IsNotEmpty()
142+
@IsOptional()
143+
readonly description?: string;
144+
145+
@ApiPropertyOptional({
146+
description: '컨텐츠 링크',
147+
})
148+
@IsUrl()
149+
@IsNotEmpty()
150+
@IsOptional()
151+
readonly link?: string;
152+
153+
@ApiPropertyOptional({
154+
description: '컨텐츠 제목',
155+
})
156+
@IsString()
157+
@IsNotEmpty()
158+
@IsOptional()
159+
readonly title?: string;
160+
161+
@ApiPropertyOptional({
162+
description: '컨텐츠 메모',
163+
})
164+
@IsString()
165+
@IsNotEmpty()
166+
@IsOptional()
167+
readonly comment?: string;
168+
169+
@ApiPropertyOptional({
170+
description: '리마인더 시간',
171+
})
172+
@Type(() => Date)
173+
@IsDate()
174+
@IsOptional()
175+
readonly reminder?: Date;
176+
177+
@ApiPropertyOptional({
178+
description: '즐겨찾기 여부',
179+
})
180+
@IsBoolean()
181+
@IsOptional()
182+
readonly favorite?: boolean;
183+
184+
@ApiPropertyOptional({
185+
description: '카테고리 id',
186+
})
187+
@IsInt()
188+
@IsPositive()
189+
@IsOptional()
190+
readonly categoryId?: number;
191+
}
192+
193+
export class UpdateContentRequest {
194+
@ApiPropertyOptional({
195+
description: '컨텐츠 설명',
196+
})
197+
@IsString()
198+
@IsNotEmpty()
199+
@IsOptional()
200+
readonly description?: string;
201+
202+
@ApiPropertyOptional({
203+
description: '컨텐츠 링크',
204+
})
205+
@IsUrl()
206+
@IsNotEmpty()
207+
@IsOptional()
208+
readonly link?: string;
209+
210+
@ApiPropertyOptional({
211+
description: '컨텐츠 제목',
212+
})
213+
@IsString()
214+
@IsNotEmpty()
215+
@IsOptional()
216+
readonly title?: string;
217+
218+
@ApiPropertyOptional({
219+
description: '컨텐츠 메모',
220+
})
221+
@IsString()
222+
@IsNotEmpty()
223+
@IsOptional()
224+
readonly comment?: string;
225+
226+
@ApiPropertyOptional({
227+
description: '리마인더 시간',
228+
})
229+
@Type(() => Date)
230+
@IsDate()
231+
@IsOptional()
232+
readonly reminder?: Date;
233+
234+
@ApiPropertyOptional({
235+
description: '즐겨찾기 여부',
236+
})
237+
@IsBoolean()
238+
@IsOptional()
239+
readonly favorite?: boolean;
240+
241+
@ApiPropertyOptional({
242+
description: '카테고리 id',
243+
})
244+
@IsInt()
245+
@IsPositive()
246+
@IsOptional()
247+
readonly categoryId?: number;
248+
}
249+
133250
export class UpdateContentOutput extends CoreOutput {}
134251

135252
export class DeleteContentOutput extends CoreOutput {}

src/contents/entities/content.entity.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,8 @@ export class Content extends CoreEntity {
8686
@ApiProperty({ description: 'Owner ID' })
8787
@RelationId((content: Content) => content.user)
8888
userId: number;
89+
90+
isSameCategory(categoryId: number): boolean {
91+
return this.category?.id === categoryId;
92+
}
8993
}

0 commit comments

Comments
 (0)