Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/controllers/__test__/user.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,4 +405,44 @@ describe('UserController', () => {
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
});

describe('unsubscribeNewsletter', () => {
beforeEach(() => {
mockRequest.query = {};
mockResponse.redirect = jest.fn().mockReturnThis();
});

it('이메일이 없으면 BadRequestError를 던져야 한다', async () => {
mockRequest.query = {};

await userController.unsubscribeNewsletter(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

expect(nextFunction).toHaveBeenCalledWith(
expect.objectContaining({
message: '이메일이 필요합니다.',
})
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});

it('구독 해제 완료시 메인 페이지로 리다이렉트해야 한다', async () => {
const email = '[email protected]';
mockRequest.query = { email };
mockUserService.unsubscribeNewsletter.mockResolvedValue(undefined);

await userController.unsubscribeNewsletter(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

expect(mockUserService.unsubscribeNewsletter).toHaveBeenCalledWith(email);
expect(mockResponse.redirect).toHaveBeenCalledWith('/main');
expect(nextFunction).not.toHaveBeenCalled();
});
});
});
17 changes: 16 additions & 1 deletion src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import logger from '@/configs/logger.config';
import { EmptyResponseDto, LoginResponseDto } from '@/types';
import { QRLoginTokenResponseDto } from '@/types/dto/responses/qrResponse.type';
import { UserService } from '@/services/user.service';
import { QRTokenExpiredError, QRTokenInvalidError } from '@/exception/token.exception';
import { fetchVelogApi } from '@/modules/velog/velog.api';
import { QRTokenExpiredError, QRTokenInvalidError, BadRequestError } from '@/exception';

type Token10 = string & { __lengthBrand: 10 };

Expand Down Expand Up @@ -169,4 +169,19 @@ export class UserController {
next(error);
}
};

unsubscribeNewsletter: RequestHandler = async (req: Request, res: Response<EmptyResponseDto>, next: NextFunction) => {
try {
const email = req.query.email as string;
if (!email) {
throw new BadRequestError('이메일이 필요합니다.');
}
Comment on lines 175 to 182
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 에러를 받으면 그러면 사용자는 어떻게 해야 하나요?


await this.userService.unsubscribeNewsletter(email);
res.redirect('/main');
} catch (error) {
logger.error(`뉴스레터 구독 해제 실패: [email: ${req.query.email}]`, error);
next(error);
}
};
}
24 changes: 22 additions & 2 deletions src/repositories/user.repository.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

타입 강화를 더 돈독히 해주셨군요. (애초에 이게 TS 린팅에 안걸리는게 더 신기하네..)

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DBError } from '@/exception';
export class UserRepository {
constructor(private readonly pool: Pool) {}

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

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

async findByUserEmail(email: string): Promise<User | null> {
try {
const user = await this.pool.query('SELECT * FROM "users_user" WHERE email = $1', [email]);
return user.rows[0] || null;
} catch (error) {
logger.error('Email로 유저를 조회 중 오류 : ', error);
throw new DBError('유저 조회 중 문제가 발생했습니다.');
}
}

async findSampleUser(): Promise<User> {
try {
const query = `
Expand Down Expand Up @@ -152,4 +162,14 @@ export class UserRepository {
throw new DBError('QR 코드 사용 처리 중 문제가 발생했습니다.');
}
}

async unsubscribeNewsletter(id: number): Promise<void> {
try {
const query = `UPDATE "users_user" SET newsletter_subscribed = false WHERE id = $1`;
await this.pool.query(query, [id]);
} catch (error) {
logger.error('User Repo unsubscribeNewsletter Error : ', error);
throw new DBError('뉴스레터 구독 해제 중 문제가 발생했습니다.');
}
}
}
20 changes: 20 additions & 0 deletions src/routes/user.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,24 @@ router.post('/qr-login', authMiddleware.verify, userController.createToken);
*/
router.get('/qr-login', userController.getToken);

/**
* @swagger
* /newsletter-unsubscribe:
* get:
* tags:
* - User
* summary: 뉴스레터 구독 해제 (메일에서 바로 접근)
* parameters:
* - in: query
* name: email
* required: true
* schema:
* type: string
* description: 구독을 해제할 이메일
* responses:
* 302:
* description: 뉴스레터 구독 해제 성공 후 메인 페이지로 리디렉션
*/
router.get('/newsletter-unsubscribe', userController.unsubscribeNewsletter);

export default router;
15 changes: 15 additions & 0 deletions src/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,19 @@ export class UserService {
);
return { decryptedAccessToken, decryptedRefreshToken };
}

async unsubscribeNewsletter(email: string) {
try {
const user = await this.userRepo.findByUserEmail(email);
if (!user) {
logger.error(`유저를 찾을 수 없습니다. [email: ${email}]`);
return; // 일반적인 실패시 리디렉션
}

await this.userRepo.unsubscribeNewsletter(user.id);
} catch (error) {
logger.error('User Service unsubscribeNewsletter Error : ', error);
throw error;
}
}
}
2 changes: 2 additions & 0 deletions src/types/models/User.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface User {
// 250607 추가
username: string | null;
thumbnail: string | null;
// 251004 추가
newsletter_subscribed: boolean;
}


Expand Down
1 change: 1 addition & 0 deletions src/utils/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const mockUser: User = {
is_active: true,
created_at: new Date('2024-01-01T00:00:00Z'),
updated_at: new Date('2024-01-01T00:00:00Z'),
newsletter_subscribed: true,
};

/**
Expand Down