Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6d3b0ec
feat: qrcode app ์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค, ๋ ˆํฌ์ง€ํ† ๋ฆฌ, ํƒ€์ž… ์ถ”๊ฐ€, ๊ด€๋ จ API ๊ตฌํ˜„
Jihyun3478 Apr 20, 2025
3b6adaa
hotfix: ์ฝ”๋“œ๋ž˜๋น— ๋ฆฌ๋ทฐ ๋ฐ˜์˜
Jihyun3478 Apr 20, 2025
b70a582
modify: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ํด๋ž˜์Šค ์‚ญ์ œ
Jihyun3478 Apr 21, 2025
56c2283
refactor: ๋ถˆํ•„์š”ํ•œ ์˜์กด์„ฑ ์‚ญ์ œ
Jihyun3478 Apr 25, 2025
ac39619
refactor: ๊ณต๋ฐฑ ์ œ๊ฑฐ
Jihyun3478 Apr 25, 2025
d118b16
refactor: ํ™œ์šฉ๋„ ๋‚ฎ์€ ํ•„๋“œ ์ˆœ์„œ ์ •๋ฆฌ
Jihyun3478 Apr 25, 2025
f6f3936
modify: token ๊ณ ์ • ๊ธธ์ด ๊ฐ’ ๋ช…์‹œ & exception ์žฌ์‚ฌ์šฉ
Jihyun3478 Apr 25, 2025
065879c
refactor: repository ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋ฉ”์„œ๋“œ ๋ถ„๋ฆฌ
Jihyun3478 Apr 25, 2025
3faf173
hotfix: uuid ์‚ญ์ œ
Jihyun3478 Apr 25, 2025
a007870
hotfix: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํ๋ฆ„์— ๋งž๊ฒŒ ์ˆ˜์ •
Jihyun3478 Apr 25, 2025
a95635e
refactor: ์ฝ”๋“œ๋ž˜๋น— ๋ฆฌ๋ทฐ ๋ฐ˜์˜
Jihyun3478 Apr 25, 2025
245a721
hotfix: token 36์ž๊ฐ€ ์•„๋‹Œ 10์ž๋กœ ์ˆ˜์ •
Jihyun3478 Apr 29, 2025
4fd26ac
hotfix: ์ฟผ๋ฆฌ ์ž‘์„ฑ ์‹œ qr_login_tokens๊ฐ€ ์•„๋‹Œ users_qrlogintoken์œผ๋กœ ์ˆ˜์ •
Jihyun3478 Apr 29, 2025
c7e9019
test: ๋ ˆํฌ์ง€ํ† ๋ฆฌ ํ†ตํ•ฉํ…Œ์ŠคํŠธ์ฝ”๋“œ ๊ตฌํ˜„
Jihyun3478 Apr 29, 2025
51a0162
Merge branch 'main' into feature/qrcode-app
Jihyun3478 Apr 29, 2025
5932dac
hotfix: ์ค‘๋ณต ํ† ํฐ ํ…Œ์ŠคํŠธ ํ›„ DB ์—ฐ๊ฒฐ ์ข…๋ฃŒ
Jihyun3478 Apr 29, 2025
85b502c
hotfix: ์ค‘๋ณต ํ† ํฐ ์‚ฝ์ž… ํ…Œ์ŠคํŠธ ์‚ญ์ œ
Jihyun3478 Apr 29, 2025
37b9633
modify: QRLoginToken ๋ผ์šฐํ„ฐ, ์„œ๋น„์Šค, ๋ ˆํฌ User์ชฝ์œผ๋กœ ํ•ฉ์น˜๊ธฐ
Jihyun3478 Apr 29, 2025
3440fa2
modify: ์ฝ”๋“œ๋ž˜๋น— ๋ฆฌ๋ทฐ 1์ฐจ ๋ฐ˜์˜
Jihyun3478 Apr 29, 2025
d2d127a
hotfix: process.env ๋Œ€์‹  ์ž„์˜์˜ ๋‚œ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •
Jihyun3478 Apr 30, 2025
fe68599
refactor: lint ์ ์šฉ
Jihyun3478 Apr 30, 2025
f8801e0
refactor: ๋“ค์—ฌ์“ฐ๊ธฐ ์ •๋ฆฌ
Jihyun3478 Apr 30, 2025
5613dbb
hotfix: ์Šฌ๋ž™๋„ mockingํ•˜๋„๋ก ์ˆ˜์ •
Jihyun3478 Apr 30, 2025
31b1a43
hotfix: ํŠน์ • ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ํ…Œ์ŠคํŠธ๊ฐ€ ์™„๋ฃŒ๋œ ํ›„ ์ง€์šฐ๋„๋ก ์ˆ˜์ •
Jihyun3478 May 1, 2025
6d6f918
docs: Swagger ์ฃผ์„ ์ˆ˜์ • ๋ฐ ์ถ”๊ฐ€
Jihyun3478 May 1, 2025
3ce8994
hotfix: ํ† ํฐ ์ƒ์„ฑ ๋กœ์ง ์ˆ˜์ •
Jihyun3478 May 1, 2025
5c8f0a9
hotfix: ์‹ค์ œ ํด๋ผ์ด์–ธํŠธ์˜ IP์— ์ ‘๊ทผํ•˜๋„๋ก ์ˆ˜์ • & logger ๊ตฌ์ฒดํ™”
Jihyun3478 May 1, 2025
0769497
refactor: ์ฝ”๋“œ๋ž˜๋น— ๋ฆฌ๋ทฐ ๋ฐ˜์˜
Jihyun3478 May 1, 2025
624be89
docs: swagger ๋ฌธ์„œ ์ˆ˜์ •
Jihyun3478 May 2, 2025
c20e48e
modify: service์˜ getByToken์ด ์•„๋‹Œ repo์˜ findQRLoginToken์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •
Jihyun3478 May 2, 2025
b08d834
refactor: findByVelogUUID & getDecryptedTokens ๋ฉ”์„œ๋“œ ๋ณ‘ํ•ฉ
Jihyun3478 May 3, 2025
21764ee
refactor: ๋ ˆํฌ ๊ณ„์ธต๊ณผ์˜ ์ค‘๋ณต ํ…Œ์ŠคํŠธ์ฝ”๋“œ ์ œ๊ฑฐ
Jihyun3478 May 3, 2025
c2dd34a
refactor: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” import & ์ฝ”๋“œ ์ œ๊ฑฐ
Jihyun3478 May 3, 2025
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"reflect-metadata": "^0.2.2",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"uuid": "^11.1.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 54 additions & 0 deletions src/controllers/qr.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { NextFunction, Request, RequestHandler, Response } from 'express';
import logger from '@/configs/logger.config';
import { QRLoginTokenService } from "@/services/qr.service";
import { QRLoginTokenResponseDto } from "@/types/dto/responses/qrResponse.type";

export class QRLoginController {
constructor(private qrService: QRLoginTokenService) {}

createToken: RequestHandler = async (
req: Request,
res: Response<QRLoginTokenResponseDto>,
next: NextFunction,
) => {
try {
const user = req.user;
const ip = req.ip ?? '';
const userAgent = req.headers['user-agent'] || '';

const token = await this.qrService.create(user.id, ip, userAgent);

const response = new QRLoginTokenResponseDto(
true,
'QR ํ† ํฐ ์ƒ์„ฑ ์™„๋ฃŒ',
{ token: token },
null
);
res.status(200).json(response);
} catch (error) {
logger.error('์ƒ์„ฑ ์‹คํŒจ:', error);
next(error);
}
};

getToken: RequestHandler = async (req, res, next) => {
try {
const token = req.query.token as string;

if (!token) {
res.status(400).json({ success: false, message: 'ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.' });
}

const found = await this.qrService.getByToken(token);

if (!found) {
res.status(404).json({ success: false, message: '์œ ํšจํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋œ ํ† ํฐ์ž…๋‹ˆ๋‹ค.' });
}

res.status(200).json({ success: true, message: '์œ ํšจํ•œ QR ํ† ํฐ์ž…๋‹ˆ๋‹ค.', token: found });
Copy link
Contributor

Choose a reason for hiding this comment

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

๐Ÿ› ๏ธ Refactor suggestion

๋ณด์•ˆ์„ ์œ„ํ•ด ํ† ํฐ ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ์ œํ•œํ•˜์„ธ์š”.

ํ˜„์žฌ ์‘๋‹ต์—๋Š” ์ „์ฒด ํ† ํฐ ๊ฐ์ฒด๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์–ด ๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•„์š”ํ•œ ์ •๋ณด๋งŒ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ์ œํ•œํ•˜์„ธ์š”:

-      res.status(200).json({ success: true, message: '์œ ํšจํ•œ QR ํ† ํฐ์ž…๋‹ˆ๋‹ค.', token: found });
+      res.status(200).json({
+        success: true,
+        message: '์œ ํšจํ•œ QR ํ† ํฐ์ž…๋‹ˆ๋‹ค.',
+        data: {
+          token: found.token,
+          user: found.user,
+          expires_at: found.expires_at
+        }
+      });
๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res.status(200).json({ success: true, message: '์œ ํšจํ•œ QR ํ† ํฐ์ž…๋‹ˆ๋‹ค.', token: found });
res.status(200).json({
success: true,
message: '์œ ํšจํ•œ QR ํ† ํฐ์ž…๋‹ˆ๋‹ค.',
data: {
token: found.token,
user: found.user,
expires_at: found.expires_at
}
});

} catch (error) {
logger.error('QR ํ† ํฐ ์กฐํšŒ ์‹คํŒจ', error);
next(error);
}
};
}
77 changes: 77 additions & 0 deletions src/repositories/__test__/qr.repo.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import dotenv from 'dotenv';
import { Pool } from 'pg';
import pg from 'pg';
import { QRLoginTokenRepository } from '@/repositories/qr.repository';
import { v4 as uuidv4 } from 'uuid';

dotenv.config();
jest.setTimeout(30000);

describe('QRLoginTokenRepository Integration Test', () => {
let pool: Pool;
let repo: QRLoginTokenRepository;

beforeAll(async () => {
try {
const testPoolConfig: pg.PoolConfig = {
user: process.env.POSTGRES_USER,
host: process.env.POSTGRES_HOST,
database: process.env.DATABASE_NAME,
password: process.env.POSTGRES_PASSWORD,
port: Number(process.env.POSTGRES_PORT),
max: 1,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
statement_timeout: 30000,
};

if (process.env.POSTGRES_HOST !== 'localhost') {
testPoolConfig.ssl = {
rejectUnauthorized: false,
};
}

pool = new Pool(testPoolConfig);
await pool.query('SELECT 1'); // ์—ฐ๊ฒฐ ํ™•์ธ
console.info('QR ํ…Œ์ŠคํŠธ DB ์—ฐ๊ฒฐ ์„ฑ๊ณต');

// ํ•„์š”ํ•œ ํ…Œ์ด๋ธ” ๋ฐ ์œ ์ € ์กด์žฌ ํ™•์ธ (์„ ํƒ ์‚ฌํ•ญ)
const tableCheck = await pool.query(`SELECT to_regclass('qr_login_tokens')`);
if (!tableCheck.rows[0].to_regclass) {
throw new Error('qr_login_tokens ํ…Œ์ด๋ธ”์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
}

const userCheck = await pool.query(`SELECT COUNT(*) FROM users WHERE id = $1`, [1]);
if (parseInt(userCheck.rows[0].count) === 0) {
throw new Error('user_id = 1 ์œ ์ €๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
}

repo = new QRLoginTokenRepository(pool);
} catch (error) {
console.error('QR ํ…Œ์ŠคํŠธ ์„ค์ • ์ค‘ ์˜ค๋ฅ˜:', error);
throw error;
}
});

afterAll(async () => {
await pool.end();
});

it('should insert and retrieve QR token', async () => {
const token = uuidv4();
const userId = 1;
const ip = '127.0.0.1';
const userAgent = 'test-agent';

await repo.createQRLoginToken(token, userId, ip, userAgent);
const result = await repo.findQRLoginToken(token);

expect(result).not.toBeNull();
expect(result?.token).toBe(token);
});

it('should return null for expired or used token', async () => {
const result = await repo.findQRLoginToken('invalid-token');
expect(result).toBeNull();
});
});
56 changes: 56 additions & 0 deletions src/repositories/__test__/qr.repo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { QRLoginTokenRepository } from '@/repositories/qr.repository';
import { DBError } from '@/exception';
import { Pool } from 'pg';

const mockPool: Partial<Pool> = {
query: jest.fn()
};

describe('QRLoginTokenRepository', () => {
let repo: QRLoginTokenRepository;

beforeEach(() => {
repo = new QRLoginTokenRepository(mockPool as Pool);
});

afterEach(() => {
jest.clearAllMocks();
});

it('should insert QR login token', async () => {
(mockPool.query as jest.Mock).mockResolvedValueOnce(undefined);

await expect(
repo.createQRLoginToken('token', 1, 'ip', 'agent')
).resolves.not.toThrow();

expect(mockPool.query).toHaveBeenCalled();
});

it('should throw DBError on insert failure', async () => {
(mockPool.query as jest.Mock).mockRejectedValueOnce(new Error('fail'));

await expect(repo.createQRLoginToken('token', 1, 'ip', 'agent'))
.rejects.toThrow(DBError);
});

it('should return token if found', async () => {
(mockPool.query as jest.Mock).mockResolvedValueOnce({ rows: [{ token: 'token' }] });

const result = await repo.findQRLoginToken('token');
expect(result).toEqual({ token: 'token' });
});

it('should return null if token not found', async () => {
(mockPool.query as jest.Mock).mockResolvedValueOnce({ rows: [] });

const result = await repo.findQRLoginToken('token');
expect(result).toBeNull();
});

it('should throw DBError on select failure', async () => {
(mockPool.query as jest.Mock).mockRejectedValueOnce(new Error('fail'));

await expect(repo.findQRLoginToken('token')).rejects.toThrow(DBError);
});
});
36 changes: 36 additions & 0 deletions src/repositories/qr.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Pool } from 'pg';
import logger from '@/configs/logger.config';
import { DBError } from '@/exception';
import { QRLoginToken } from '@/types/models/QRLoginToken.type';

export class QRLoginTokenRepository {
constructor(private pool: Pool) { }

async createQRLoginToken(token: string, userId: number, ip: string, userAgent: string): Promise<void> {
try {
const query = `
INSERT INTO qr_login_tokens (token, user_id, created_at, expires_at, is_used, ip_address, user_agent)
VALUES ($1, $2, NOW(), NOW() + INTERVAL '5 minutes', false, $3, $4);
`;
await this.pool.query(query, [token, userId, ip, userAgent]);
} catch (error) {
logger.error('QRLoginToken Repo Create Error : ', error);
throw new DBError('QR ์ฝ”๋“œ ํ† ํฐ ์ƒ์„ฑ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
}
}

async findQRLoginToken(token: string): Promise<QRLoginToken | null> {
try {
const query = `
SELECT *
FROM qr_login_tokens
WHERE token = $1 AND is_used = false AND expires_at > NOW();
`;
const result = await this.pool.query(query, [token]);
return result.rows[0] ?? null;
} catch (error) {
logger.error('QRLoginToken Repo find QR Code Error : ', error);
throw new DBError('QR ์ฝ”๋“œ ํ† ํฐ ์กฐํšŒ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
}
}
}
3 changes: 3 additions & 0 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import express, { Router } from 'express';
import UserRouter from './user.router';
import PostRouter from './post.router';
import NotiRouter from './noti.router';
import QrRouter from './qr.router';

const router: Router = express.Router();

Expand All @@ -12,4 +13,6 @@ router.use('/ping', (req, res) => {
router.use('/', UserRouter);
router.use('/', PostRouter);
router.use('/', NotiRouter);
router.use('/', QrRouter);

export default router;
50 changes: 50 additions & 0 deletions src/routes/qr.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import express, { Router } from 'express';
import pool from '@/configs/db.config';

import { authMiddleware } from '@/middlewares/auth.middleware';
import { QRLoginTokenRepository } from '@/repositories/qr.repository';
import { QRLoginTokenService } from '@/services/qr.service';
import { QRLoginController } from '@/controllers/qr.controller';

const router: Router = express.Router();

const qrRepository = new QRLoginTokenRepository(pool);
const qrService = new QRLoginTokenService(qrRepository);
const qrController = new QRLoginController(qrService);

/**
* @swagger
* /api/qr-login:
* post:
* summary: QR ๋กœ๊ทธ์ธ ํ† ํฐ ์ƒ์„ฑ
* tags: [QRLogin]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: QR ๋กœ๊ทธ์ธ ํ† ํฐ ์ƒ์„ฑ ์„ฑ๊ณต
*/
router.post('/qr-login', authMiddleware.login, qrController.createToken);

/**
* @swagger
* /api/qr-login:
* get:
* summary: QR ๋กœ๊ทธ์ธ ํ† ํฐ ์กฐํšŒ
* tags: [QRLogin]
* parameters:
* - in: query
* name: token
* required: true
* schema:
* type: string
* description: ์กฐํšŒํ•  QR ํ† ํฐ
* responses:
* 200:
* description: ์œ ํšจํ•œ ํ† ํฐ
* 404:
* description: ํ† ํฐ ์—†์Œ or ๋งŒ๋ฃŒ
*/
router.get('/qr-login', qrController.getToken);

export default router;
Loading
Loading