Skip to content

Commit a7a7ff9

Browse files
committed
feature: 뉴스레터 수신 거부 API 추가
1 parent e29d2ba commit a7a7ff9

File tree

5 files changed

+114
-2
lines changed

5 files changed

+114
-2
lines changed

src/controllers/__test__/user.controller.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,4 +405,44 @@ describe('UserController', () => {
405405
expect(mockResponse.redirect).not.toHaveBeenCalled();
406406
});
407407
});
408+
409+
describe('unsubscribeNewsletter', () => {
410+
beforeEach(() => {
411+
mockRequest.query = {};
412+
mockResponse.redirect = jest.fn().mockReturnThis();
413+
});
414+
415+
it('이메일이 없으면 BadRequestError를 던져야 한다', async () => {
416+
mockRequest.query = {};
417+
418+
await userController.unsubscribeNewsletter(
419+
mockRequest as Request,
420+
mockResponse as Response,
421+
nextFunction
422+
);
423+
424+
expect(nextFunction).toHaveBeenCalledWith(
425+
expect.objectContaining({
426+
message: '이메일이 필요합니다.',
427+
})
428+
);
429+
expect(mockResponse.redirect).not.toHaveBeenCalled();
430+
});
431+
432+
it('구독 해제 완료시 메인 페이지로 리다이렉트해야 한다', async () => {
433+
const email = 'test@example.com';
434+
mockRequest.query = { email };
435+
mockUserService.unsubscribeNewsletter.mockResolvedValue(undefined);
436+
437+
await userController.unsubscribeNewsletter(
438+
mockRequest as Request,
439+
mockResponse as Response,
440+
nextFunction
441+
);
442+
443+
expect(mockUserService.unsubscribeNewsletter).toHaveBeenCalledWith(email);
444+
expect(mockResponse.redirect).toHaveBeenCalledWith('/main');
445+
expect(nextFunction).not.toHaveBeenCalled();
446+
});
447+
});
408448
});

src/controllers/user.controller.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { QRLoginTokenResponseDto } from '@/types/dto/responses/qrResponse.type';
55
import { UserService } from '@/services/user.service';
66
import { QRTokenExpiredError, QRTokenInvalidError } from '@/exception/token.exception';
77
import { fetchVelogApi } from '@/modules/velog/velog.api';
8+
import { BadRequestError } from '@/exception';
89

910
type Token10 = string & { __lengthBrand: 10 };
1011

@@ -169,4 +170,19 @@ export class UserController {
169170
next(error);
170171
}
171172
};
173+
174+
unsubscribeNewsletter: RequestHandler = async (req: Request, res: Response<EmptyResponseDto>, next: NextFunction) => {
175+
try {
176+
const email = req.query.email as string;
177+
if (!email) {
178+
throw new BadRequestError('이메일이 필요합니다.');
179+
}
180+
181+
await this.userService.unsubscribeNewsletter(email);
182+
res.redirect('/main');
183+
} catch (error) {
184+
logger.error(`뉴스레터 구독 해제 실패: [email: ${req.query.email}]`, error);
185+
next(error);
186+
}
187+
};
172188
}

src/repositories/user.repository.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { DBError } from '@/exception';
77
export class UserRepository {
88
constructor(private readonly pool: Pool) {}
99

10-
async findByUserId(id: number): Promise<User> {
10+
async findByUserId(id: number): Promise<User | null> {
1111
try {
1212
const user = await this.pool.query('SELECT * FROM "users_user" WHERE id = $1', [id]);
1313
return user.rows[0] || null;
@@ -17,7 +17,7 @@ export class UserRepository {
1717
}
1818
}
1919

20-
async findByUserVelogUUID(uuid: string): Promise<User> {
20+
async findByUserVelogUUID(uuid: string): Promise<User | null> {
2121
try {
2222
const user = await this.pool.query('SELECT * FROM "users_user" WHERE velog_uuid = $1', [uuid]);
2323
return user.rows[0] || null;
@@ -27,6 +27,16 @@ export class UserRepository {
2727
}
2828
}
2929

30+
async findByUserEmail(email: string): Promise<User | null> {
31+
try {
32+
const user = await this.pool.query('SELECT * FROM "users_user" WHERE email = $1', [email]);
33+
return user.rows[0] || null;
34+
} catch (error) {
35+
logger.error('Email로 유저를 조회 중 오류 : ', error);
36+
throw new DBError('유저 조회 중 문제가 발생했습니다.');
37+
}
38+
}
39+
3040
async findSampleUser(): Promise<User> {
3141
try {
3242
const query = `
@@ -152,4 +162,14 @@ export class UserRepository {
152162
throw new DBError('QR 코드 사용 처리 중 문제가 발생했습니다.');
153163
}
154164
}
165+
166+
async unsubscribeNewsletter(id: number): Promise<void> {
167+
try {
168+
const query = `UPDATE "users_user" SET newsletter_subscribed = false WHERE id = $1`;
169+
await this.pool.query(query, [id]);
170+
} catch (error) {
171+
logger.error('User Repo unsubscribeNewsletter Error : ', error);
172+
throw new DBError('뉴스레터 구독 해제 중 문제가 발생했습니다.');
173+
}
174+
}
155175
}

src/routes/user.router.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,25 @@ router.post('/qr-login', authMiddleware.verify, userController.createToken);
151151
*/
152152
router.get('/qr-login', userController.getToken);
153153

154+
/**
155+
* @swagger
156+
* /newsletter-unsubscribe:
157+
* get:
158+
* tags:
159+
* - User
160+
* summary: 뉴스레터 구독 해제 (메일에서 바로 접근)
161+
* parameters:
162+
* - in: query
163+
* name: email
164+
* required: true
165+
* schema:
166+
* type: string
167+
* description: 구독을 해제할 이메일
168+
* responses:
169+
* 302:
170+
* description: 뉴스레터 구독 해제 성공 후 메인 페이지로 리디렉션
171+
*/
172+
router.get('/newsletter-unsubscribe', userController.unsubscribeNewsletter);
173+
174+
154175
export default router;

src/services/user.service.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,19 @@ export class UserService {
170170
);
171171
return { decryptedAccessToken, decryptedRefreshToken };
172172
}
173+
174+
async unsubscribeNewsletter(email: string) {
175+
try {
176+
const user = await this.userRepo.findByUserEmail(email);
177+
if (!user) {
178+
logger.error(`유저를 찾을 수 없습니다. [email: ${email}]`);
179+
return; // 일반적인 실패시 리디렉션
180+
}
181+
182+
await this.userRepo.unsubscribeNewsletter(user.id);
183+
} catch (error) {
184+
logger.error('User Service unsubscribeNewsletter Error : ', error);
185+
throw error;
186+
}
187+
}
173188
}

0 commit comments

Comments
 (0)