diff --git a/package-lock.json b/package-lock.json index 448473fc338659e4496c9a27fdd83cb07b4f7631..f1a829f4835b14bfaf1fc4ec63b0da4f4cd343f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "UNLICENSED", "dependencies": { "bcrypt": "^5.1.1", + "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^4.21.1", "jsonwebtoken": "^9.0.2", @@ -884,6 +885,13 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", diff --git a/package.json b/package.json index 24f1940ebc1ccb2fe8b565330bcbd2b2e8d284e5..d98cdc67ad06dfa40e76eb7aff72bb291745eccd 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "bcrypt": "^5.1.1", + "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^4.21.1", "jsonwebtoken": "^9.0.2", diff --git a/src/middlewares/authMiddleware.js b/src/middlewares/authMiddleware.js new file mode 100644 index 0000000000000000000000000000000000000000..50028dba88faa529a30b1a0448bc3e2ad34e8e48 --- /dev/null +++ b/src/middlewares/authMiddleware.js @@ -0,0 +1,16 @@ +import jwt from 'jsonwebtoken'; + +const authMiddleware = (req, res, next) => { + const token = req.headers['authorization']?.split(' ')[1]; + if (!token) return res.status(401).send({ message: '토큰이 필요합니다.' }); + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + req.user = decoded; + next(); + } catch { + res.status(401).send({ message: '유효하지 않은 토큰입니다.' }); + } +}; + +export default authMiddleware; diff --git a/src/repositories/myRepository.js b/src/repositories/myRepository.js new file mode 100644 index 0000000000000000000000000000000000000000..983814b1ab17a37e0cccaa5eb74fec134cac47ff --- /dev/null +++ b/src/repositories/myRepository.js @@ -0,0 +1,50 @@ +import pool from '../db.js'; + +const myRepository = { + async getCombinationsByUserId(userId) { + const query = ` + SELECT id, name + FROM combinations + WHERE owner_id = $1 + `; + const values = [userId]; + + const { rows } = await pool.query(query, values); + return rows; + }, + + async getPartIdsByCombinationId(combinationId) { + const query = ` + SELECT part_id + FROM relations + WHERE combination_id = $1 + `; + const values = [combinationId]; + + const { rows } = await pool.query(query, values); + return rows.map((row) => row.part_id); + }, + + async saveTransaction(userId, transactionId) { + const query = ` + INSERT INTO transactions (user_id, id, created_at, updated_at) + VALUES ($1, $2, NOW(), NOW()) + `; + const values = [userId, transactionId]; + await pool.query(query, values); + }, + + async getCombinationIdByTransactionId(transactionId) { + const query = ` + SELECT combination_id + FROM Pcregister_Transection + WHERE transection_id = $1 + `; + const values = [transactionId]; + const { rows } = await pool.query(query, values); + + return rows.length > 0 ? rows[0].combination_id : null; + }, +}; + +export default myRepository; diff --git a/src/routes/my.js b/src/routes/my.js index 17647e6897c272c33d65b4f4101ca07a88fe4403..bcf9439c6dc103141e348f811fb12000804fd1e6 100644 --- a/src/routes/my.js +++ b/src/routes/my.js @@ -1,9 +1,75 @@ import { Router } from 'express'; +import myService from '../services/myService.js'; +import authMiddleware from '../middlewares/authMiddleware.js'; +import { wrapAsync } from '../utils.js'; +import { ReportableError } from '../errors.js'; const myRouter = Router(); -myRouter.get('/', (req, res) => { - return res.send({ message: 'mypage things' }); -}); +myRouter.get( + '/pc', + authMiddleware, + wrapAsync(async (req, res) => { + const userId = req.user?.userId; + + if (!userId) { + throw new ReportableError(400, '유효한 사용자 정보가 없습니다.'); + } + + const data = await myService.getUserPCs(userId); + res.sendResponse('', 200, data); + }) +); + +myRouter.get( + '/registration-code', + authMiddleware, + wrapAsync(async (req, res) => { + const userId = req.user?.userId; + + if (!userId) { + throw new ReportableError(400, '사용자 ID가 없습니다.'); + } + + const code = await myService.createRegistrationCode(userId); + res.sendResponse('', 200, code); + }) +); + +myRouter.get( + '/registration-code/:code', + authMiddleware, + wrapAsync(async (req, res) => { + const { code } = req.params; + + if (!code) { + throw new ReportableError(400, '코드가 필요합니다.'); + } + + const data = await myService.getCombinationId(code); + res.sendResponse('', 200, data); + }) +); + +myRouter.post( + '/', + wrapAsync(async (req, res) => { + const authorizationHeader = req.headers['authorization']; + const code = authorizationHeader?.split(' ')[1]; + + if (!code) { + throw new ReportableError(401, '코드가 필요합니다.'); + } + + const { xml } = req.body; + + if (!xml) { + throw new ReportableError(400, 'XML 데이터가 필요합니다.'); + } + + await myService.saveDocument(code, xml); + res.sendResponse('', 200, {}); + }) +); export default myRouter; diff --git a/src/services/myService.js b/src/services/myService.js new file mode 100644 index 0000000000000000000000000000000000000000..7fb02afe57e0b1c453b23b3e7c4a4ea216ea1e2a --- /dev/null +++ b/src/services/myService.js @@ -0,0 +1,129 @@ +import { Redis } from '../redis.js'; +import { ReportableError } from '../errors.js'; +import myRepository from '../repositories/myRepository.js'; +import crypto from 'crypto'; + +const myService = { + generateCode() { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let code = ''; + for (let i = 0; i < 12; i++) { + code += chars[Math.floor(Math.random() * chars.length)]; + } + return code; + }, + + async getUserIdFromCode(code) { + const redisKey = `mypc:code:${code}:user_id`; + const userId = await Redis.get(redisKey); + + if (!userId) { + throw new ReportableError(401, '유효하지 않은 코드입니다.'); + } + + return userId; + }, + + async saveDocument(code, xml) { + const redisKey = `mypc:code:${code}:document`; + const userIdKey = `mypc:code:${code}:user_id`; + const queueKey = 'mypc:queue'; + const transactionIdKey = `mypc:code:${code}:transaction_id`; + + const transactionId = crypto.randomBytes(6).toString('hex'); + + try { + const userId = await Redis.get(userIdKey); + if (!userId) { + throw new ReportableError( + 404, + '해당 코드와 연결된 사용자 ID를 찾을 수 없습니다.' + ); + } + + await Redis.set(redisKey, xml, 'EX', 600); + + await Redis.set(transactionIdKey, transactionId, 'EX', 600); + + await Redis.lPush(queueKey, code); + + await myRepository.saveTransaction(userId, transactionId); + } catch (err) { + throw new ReportableError(500, err.toString()); + } + }, + + async createRegistrationCode(userId) { + if (!userId) { + throw new ReportableError(400, '사용자 ID가 필요합니다.'); + } + + const code = this.generateCode(); + const redisKey = `mypc:code:${code}:user_id`; + const redisValue = userId; + + try { + await Redis.set(redisKey, redisValue, 'EX', 600); + } catch { + throw new ReportableError( + 500, + '등록 코드를 생성하는 중 문제가 발생했습니다.' + ); + } + + return { code }; + }, + + async getCombinationId(req, res) { + const { code } = req.params; + const transactionIdKey = `mypc:code:${code}:transectionId`; + + try { + const transactionId = await Redis.get(transactionIdKey); + if (!transactionId) { + throw new ReportableError( + 404, + '해당 코드와 연결된 트랜잭션 ID를 찾을 수 없습니다.' + ); + } + + const combinationId = + await myRepository.getCombinationIdByTransactionId(transactionId); + + res.sendResponse('', 200, { + combinationId: combinationId || null, + }); + } catch { + throw new ReportableError( + 500, + '등록 코드 상태를 확인하는 중 문제가 발생했습니다.' + ); + } + }, + + async getUserPCs(userId) { + if (!userId) { + throw new ReportableError(400, '유효한 사용자 ID가 필요합니다.'); + } + + const combinations = await myRepository.getCombinationsByUserId(userId); + + const pcs = await Promise.all( + combinations.map(async (combination) => { + const partIds = await myRepository.getPartIdsByCombinationId( + combination?.id + ); + + return { + id: combination.id, + name: combination.name, + parts: partIds, + }; + }) + ); + + return pcs; + }, +}; + +export default myService; diff --git a/yarn.lock b/yarn.lock index bfe2fbc88cbe68155a0db0402e0f060f9a3d3c5c..bb8b2eab832493219ae86f6d2a53d2fb3a785b8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -472,6 +472,11 @@ cross-spawn@^7.0.5: shebang-command "^2.0.0" which "^2.0.1" +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== + data-view-buffer@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz"