Skip to content

Commit 2a58f94

Browse files
authored
Merge pull request #434 from boostcampwm-2024/feat/rss-remove-delete
✨ feat: RSS 삭제 인증 API 구현
2 parents eab21ca + 8a74d5e commit 2a58f94

File tree

9 files changed

+205
-7
lines changed

9 files changed

+205
-7
lines changed

server/src/migration/1752060032219-RenameLikeForeignKey.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ export class RenameLikeForeignKey1752060032219 implements MigrationInterface {
77
await queryRunner.query('ALTER TABLE likes DROP INDEX UQ_likes_user_feed;');
88

99
await queryRunner.query(
10-
'ALTER TABLE likes ADD CONSTRAINT FK_85b0dbd1e7836d0f8cdc38fe830 FOREIGN KEY (feed_id) REFERENCES feed(id);',
10+
'ALTER TABLE likes ADD CONSTRAINT FK_85b0dbd1e7836d0f8cdc38fe830 FOREIGN KEY (feed_id) REFERENCES feed(id) ON DELETE CASCADE ON UPDATE CASCADE;',
1111
);
1212
await queryRunner.query(
13-
'ALTER TABLE likes ADD CONSTRAINT FK_3f519ed95f775c781a254089171 FOREIGN KEY (user_id) REFERENCES user(id);',
13+
'ALTER TABLE likes ADD CONSTRAINT FK_3f519ed95f775c781a254089171 FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE;',
1414
);
1515
await queryRunner.query(
1616
'ALTER TABLE likes ADD CONSTRAINT IDX_0be1d6ca115f56ed76c65e6bda UNIQUE (`user_id`,`feed_id`);',
@@ -29,10 +29,10 @@ export class RenameLikeForeignKey1752060032219 implements MigrationInterface {
2929
);
3030

3131
await queryRunner.query(
32-
'ALTER TABLE likes ADD CONSTRAINT FK_like_feed FOREIGN KEY (feed_id) REFERENCES feed(id);',
32+
'ALTER TABLE likes ADD CONSTRAINT FK_like_feed FOREIGN KEY (feed_id) REFERENCES feed(id) ON DELETE CASCADE ON UPDATE CASCADE;',
3333
);
3434
await queryRunner.query(
35-
'ALTER TABLE likes ADD CONSTRAINT FK_like_user FOREIGN KEY (user_id) REFERENCES user(id);',
35+
'ALTER TABLE likes ADD CONSTRAINT FK_like_user FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE;',
3636
);
3737
await queryRunner.query(
3838
'ALTER TABLE likes ADD CONSTRAINT UQ_likes_user_feed UNIQUE (`user_id`,`feed_id`);',
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { applyDecorators } from '@nestjs/common';
2+
import {
3+
ApiBadRequestResponse,
4+
ApiNotFoundResponse,
5+
ApiOkResponse,
6+
ApiOperation,
7+
} from '@nestjs/swagger';
8+
9+
export function ApiDeleteRss() {
10+
return applyDecorators(
11+
ApiOperation({
12+
summary: 'RSS 삭제 인증 API',
13+
}),
14+
ApiNotFoundResponse({
15+
description: 'Not Found',
16+
example: {
17+
message: 'RSS 삭제 요청을 찾을 수 없습니다.',
18+
},
19+
}),
20+
ApiBadRequestResponse({
21+
description: 'Bad Request',
22+
example: {
23+
message: '오류 메세지',
24+
},
25+
}),
26+
ApiOkResponse({
27+
description: 'Ok',
28+
example: {
29+
message: 'RSS 삭제를 성공했습니다.',
30+
},
31+
}),
32+
);
33+
}

server/src/rss/controller/rss.controller.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
Body,
33
Controller,
4+
Delete,
45
Get,
56
HttpCode,
67
HttpStatus,
@@ -22,7 +23,9 @@ import { ApiReadRejectHistory } from '../api-docs/readRejectHistoryRss.api-docs'
2223
import { ApiReadAllRss } from '../api-docs/readAllRss.api-docs';
2324
import { ApiRejectRss } from '../api-docs/rejectRss.api-docs';
2425
import { ApiRequestDeleteRss } from '../api-docs/requestDeleteRss.api-docs';
25-
import { RequestDeleteRssDto } from '../dto/request/rss-remove.dto';
26+
import { RequestDeleteRssDto } from '../dto/request/rss-request-delete.dto';
27+
import { ApiDeleteRss } from '../api-docs/deleteRss.api-docs';
28+
import { DeleteRssDto } from '../dto/request/rss-delete.dto';
2629

2730
@ApiTags('RSS')
2831
@Controller('rss')
@@ -97,4 +100,12 @@ export class RssController {
97100
await this.rssService.requestRemove(requestDeleteRssDto);
98101
return ApiResponse.responseWithNoContent('RSS 삭제 요청을 성공했습니다.');
99102
}
103+
104+
@ApiDeleteRss()
105+
@Delete('remove/:code')
106+
@HttpCode(HttpStatus.OK)
107+
async deleteRss(@Param() deleteRssDto: DeleteRssDto) {
108+
await this.rssService.deleteRss(deleteRssDto);
109+
return ApiResponse.responseWithNoContent('RSS 삭제를 성공했습니다.');
110+
}
100111
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNotEmpty, IsString } from 'class-validator';
3+
4+
export class DeleteRssDto {
5+
@ApiProperty({
6+
description: '이메일 인증 코드를 입력해주세요.',
7+
example: 'test code',
8+
})
9+
@IsNotEmpty({
10+
message: '인증 코드를 입력해주세요.',
11+
})
12+
@IsString({
13+
message: '문자열로 입력해주세요.',
14+
})
15+
code: string;
16+
17+
constructor(partial: Partial<DeleteRssDto>) {
18+
Object.assign(this, partial);
19+
}
20+
}
File renamed without changes.

server/src/rss/service/rss.service.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import { RssAcceptHistoryResponseDto } from '../dto/response/rss-accept-history.
1919
import { RssRejectHistoryResponseDto } from '../dto/response/rss-reject-history.dto';
2020
import { RssManagementRequestDto } from '../dto/request/rss-management.dto';
2121
import { RejectRssRequestDto } from '../dto/request/rss-reject.dto';
22-
import { RequestDeleteRssDto } from '../dto/request/rss-remove.dto';
22+
import { RequestDeleteRssDto } from '../dto/request/rss-request-delete.dto';
2323
import { RedisService } from '../../common/redis/redis.service';
24+
import { DeleteRssDto } from '../dto/request/rss-delete.dto';
25+
import { FeedRepository } from '../../feed/repository/feed.repository';
2426

2527
@Injectable()
2628
export class RssService {
@@ -32,6 +34,7 @@ export class RssService {
3234
private readonly dataSource: DataSource,
3335
private readonly feedCrawlerService: FeedCrawlerService,
3436
private readonly redisService: RedisService,
37+
private readonly feedRepository: FeedRepository,
3538
) {}
3639

3740
async createRss(rssRegisterBodyDto: RssRegisterRequestDto) {
@@ -226,4 +229,33 @@ export class RssService {
226229

227230
return result;
228231
}
232+
233+
async deleteRss(deleteRssDto: DeleteRssDto) {
234+
const rssUrl = await this.redisService.get(
235+
`rss:remove:${deleteRssDto.code}`,
236+
);
237+
238+
if (!rssUrl) {
239+
throw new NotFoundException(
240+
'RSS 삭제 요청 인증 코드가 만료되었거나 찾을 수 없습니다.',
241+
);
242+
}
243+
244+
const rss = await this.rssAcceptRepository.findOne({
245+
where: {
246+
rssUrl: rssUrl,
247+
},
248+
});
249+
250+
if (!rss) {
251+
await this.redisService.del(`rss:remove;${deleteRssDto.code}`);
252+
throw new NotFoundException('이미 지워진 RSS 정보입니다.');
253+
}
254+
255+
await this.feedRepository.delete({
256+
blog: {
257+
id: rss.id,
258+
},
259+
});
260+
}
229261
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { validate } from 'class-validator';
2+
import { DeleteRssDto } from '../../../src/rss/dto/request/rss-delete.dto';
3+
4+
describe('DeleteRssDto Test', () => {
5+
let dto: DeleteRssDto;
6+
beforeEach(() => {
7+
dto = new DeleteRssDto({
8+
code: 'test',
9+
});
10+
});
11+
12+
it('인증 코드가 문자열이 아니다.', async () => {
13+
// given
14+
dto.code = 1234 as any;
15+
16+
// when
17+
const errors = await validate(dto);
18+
19+
// then
20+
expect(errors[0].constraints).toHaveProperty(
21+
'isString',
22+
'문자열로 입력해주세요.',
23+
);
24+
});
25+
it('인증 코드가 없다.', async () => {
26+
// given
27+
delete dto.code;
28+
29+
// when
30+
const errors = await validate(dto);
31+
32+
// then
33+
expect(errors[0].constraints).toHaveProperty(
34+
'isNotEmpty',
35+
'인증 코드를 입력해주세요.',
36+
);
37+
});
38+
});

server/test/rss/dto/rss-remove.dto.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { validate } from 'class-validator';
2-
import { RequestDeleteRssDto } from '../../../src/rss/dto/request/rss-remove.dto';
2+
import { RequestDeleteRssDto } from '../../../src/rss/dto/request/rss-request-delete.dto';
33

44
describe('RequestDeleteRssDto Test', () => {
55
let dto: RequestDeleteRssDto;

server/test/rss/e2e/remove.e2e-spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,31 @@ import {
55
} from '../../../src/rss/repository/rss.repository';
66
import { RssFixture } from '../../fixture/rss.fixture';
77
import * as request from 'supertest';
8+
import { RedisService } from '../../../src/common/redis/redis.service';
9+
import { FeedRepository } from '../../../src/feed/repository/feed.repository';
10+
import { FeedFixture } from '../../fixture/feed.fixture';
11+
import { CommentRepository } from '../../../src/comment/repository/comment.repository';
12+
import { CommentFixture } from '../../fixture/comment.fixture';
13+
import { UserRepository } from '../../../src/user/repository/user.repository';
14+
import { UserFixture } from '../../fixture/user.fixture';
815

916
describe('/api/rss/remove E2E Test', () => {
1017
let app: INestApplication;
1118
let rssRepository: RssRepository;
19+
let feedRepository: FeedRepository;
1220
let rssAcceptRepository: RssAcceptRepository;
21+
let redisService: RedisService;
22+
let commentRepository: CommentRepository;
23+
let userRepository: UserRepository;
1324

1425
beforeAll(() => {
1526
app = global.testApp;
1627
rssRepository = app.get(RssRepository);
1728
rssAcceptRepository = app.get(RssAcceptRepository);
29+
redisService = app.get(RedisService);
30+
feedRepository = app.get(FeedRepository);
31+
commentRepository = app.get(CommentRepository);
32+
userRepository = app.get(UserRepository);
1833
});
1934

2035
describe('POST /api/rss/remove E2E Test', () => {
@@ -66,4 +81,53 @@ describe('/api/rss/remove E2E Test', () => {
6681
expect(response.status).toBe(200);
6782
});
6883
});
84+
85+
describe('DELETE /api/rss/remove/:code', () => {
86+
it('[404] 삭제 신청된 RSS가 없으면 인증할 수 없다.', async () => {
87+
// when
88+
const response = await request(app.getHttpServer())
89+
.delete(`/api/rss/remove/testfail`)
90+
.send();
91+
92+
// then
93+
expect(response.status).toBe(404);
94+
});
95+
96+
it('[404] 이미 지워진 RSS라면 지울 수 없다.', async () => {
97+
// given
98+
await redisService.set('rss:remove:rssNotFound', 'test');
99+
100+
// when
101+
const response = await request(app.getHttpServer())
102+
.delete(`/api/rss/remove/rssNotFound`)
103+
.send();
104+
105+
// then
106+
expect(response.status).toBe(404);
107+
});
108+
109+
it('[200] 삭제 신청된 RSS가 있을 경우 좋아요, 댓글, 게시글, RSS가 한 번에 삭제된다.', async () => {
110+
// given
111+
const certificateCode = 'test';
112+
const rss = await rssAcceptRepository.save(RssFixture.createRssFixture());
113+
const feed = await feedRepository.save(
114+
FeedFixture.createFeedFixture(rss),
115+
);
116+
const user = await userRepository.save(
117+
await UserFixture.createUserCryptFixture(),
118+
);
119+
await commentRepository.save(
120+
CommentFixture.createCommentFixture(feed, user),
121+
);
122+
await redisService.set(`rss:remove:${certificateCode}`, rss.rssUrl);
123+
124+
// when
125+
const response = await request(app.getHttpServer())
126+
.delete(`/api/rss/remove/${certificateCode}`)
127+
.send();
128+
129+
// then
130+
expect(response.status).toBe(200);
131+
});
132+
});
69133
});

0 commit comments

Comments
 (0)