From 73a69b7cbb9dfb58f18f10ff701f57eca9bcd013 Mon Sep 17 00:00:00 2001 From: Eunhak Lee <lee@enak.kr> Date: Sat, 30 Nov 2024 16:50:25 +0900 Subject: [PATCH 1/8] style: eslint --fix --- src/errors.js | 2 +- src/index.js | 5 ++--- src/middlewares/responseMiddleware.js | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/errors.js b/src/errors.js index f40194a..4aa7ef8 100644 --- a/src/errors.js +++ b/src/errors.js @@ -4,4 +4,4 @@ export class ReportableError extends Error { this.message = message; this.statusCode = statusCode; } -} \ No newline at end of file +} diff --git a/src/index.js b/src/index.js index 55d8546..27de7a5 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ import express from 'express'; import apiRouter from './routes/index.js'; import pool from './db.js'; import responseMiddleware from './middlewares/responseMiddleware.js'; +import { ReportableError } from './errors.js'; const app = express(); app.use(express.json()); @@ -10,6 +11,7 @@ app.use(express.json()); app.use(responseMiddleware); app.use('/api', apiRouter); +// eslint-disable-next-line no-unused-vars app.use((err, req, res, next) => { if (err instanceof ReportableError) { const { statusCode, message } = err; @@ -20,7 +22,6 @@ app.use((err, req, res, next) => { } }); - const PORT = +process.env.PORT || 8000; console.log('Connecting to database'); @@ -32,5 +33,3 @@ pool.connect().then(async (conn) => { console.log(`Listening on PORT ${PORT}`); }); }); - - diff --git a/src/middlewares/responseMiddleware.js b/src/middlewares/responseMiddleware.js index 9d64db2..9b91e6e 100644 --- a/src/middlewares/responseMiddleware.js +++ b/src/middlewares/responseMiddleware.js @@ -9,5 +9,4 @@ const responseMiddleware = (req, res, next) => { next(); }; -export default responseMiddleware - +export default responseMiddleware; -- GitLab From 92a19e40279698073302082957263e661baf42bb Mon Sep 17 00:00:00 2001 From: Eunhak Lee <lee@enak.kr> Date: Sat, 30 Nov 2024 17:41:40 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20parts=20repo,=20service=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/repositories/partRepository.js | 22 ++++++++++++++++++++++ src/services/partService.js | 27 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/repositories/partRepository.js create mode 100644 src/services/partService.js diff --git a/src/repositories/partRepository.js b/src/repositories/partRepository.js new file mode 100644 index 0000000..43f5c04 --- /dev/null +++ b/src/repositories/partRepository.js @@ -0,0 +1,22 @@ +import pool from '../db.js'; + +const PartRepository = { + async findById(id) { + const resp = await pool.query( + `SELECT type, name, image_url FROM parts WHERE id = $1`, + [id] + ); + const [part] = resp.rows; + return part; + }, + async findMetaByTypeAndId(type, id) { + const resp = await pool.query( + `SELECT * FROM part_info_${type} WHERE part_id = $1;`, + [id] + ); + const [info] = resp.rows; + return info; + }, +}; + +export default PartRepository; diff --git a/src/services/partService.js b/src/services/partService.js new file mode 100644 index 0000000..a326299 --- /dev/null +++ b/src/services/partService.js @@ -0,0 +1,27 @@ +import { ReportableError } from '../errors.js'; +import PartRepository from '../repositories/partRepository.js'; + +const PartService = { + cleanEntity(entity) { + return entity; + }, + async getById(id) { + if (!id || id <= 0) + throw new ReportableError(400, '올바르지 ì•Šì€ id 입니다.'); + + const part = await PartRepository.findById(id); + if (!part) throw new ReportableError(404, '해당 ë¶€í’ˆì„ ì°¾ì„ ìˆ˜ 없습니다'); + + // eslint-disable-next-line no-unused-vars + const { part_id, ...infos } = await PartRepository.findMetaByTypeAndId( + part.type.toLowerCase(), + id + ); + const description = Object.values(infos).join(' / '); + part['description'] = description; + + return this.cleanEntity(part); + }, +}; + +export default PartService; -- GitLab From d0fb2c5e4309406b2e0c263b6935054512c5ec00 Mon Sep 17 00:00:00 2001 From: Eunhak Lee <lee@enak.kr> Date: Sat, 30 Nov 2024 17:42:04 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EB=B6=80=ED=92=88=20id=EB=A1=9C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/parts.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/routes/parts.js b/src/routes/parts.js index 2ea1b2c..64f03d8 100644 --- a/src/routes/parts.js +++ b/src/routes/parts.js @@ -1,9 +1,26 @@ import { Router } from 'express'; +import { wrapAsync } from '../utils.js'; +import { ReportableError } from '../errors.js'; +import PartService from '../services/partService.js'; const partRouter = Router(); -partRouter.get('/', (req, res) => { - return res.send({ message: 'parts thigns' }); -}); +partRouter.get( + '/', + wrapAsync(async (req, res) => { + return res.send({ message: 'parts thigns' }); + }) +); + +partRouter.get( + '/:id', + wrapAsync(async (req, res) => { + const { id } = req.params; + if (!id) throw ReportableError(400, 'id 를 ì§€ì •í•´ì£¼ì„¸ìš”'); + + const part = await PartService.getById(+id); + return res.sendResponse('', 200, part); + }) +); export default partRouter; -- GitLab From 1262fd3f0909c39af6eab78353462c27bcb48111 Mon Sep 17 00:00:00 2001 From: Eunhak Lee <lee@enak.kr> Date: Sat, 30 Nov 2024 17:44:50 +0900 Subject: [PATCH 4/8] =?UTF-8?q?chore:=20=EB=B6=80=ED=92=88=EC=9D=84=20id?= =?UTF-8?q?=20=EB=A1=9C=20=EA=B0=80=EC=A0=B8=EC=98=AC=20=EB=95=8C=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=EA=B9=8C=EC=A7=80=20=EA=B0=80=EC=A0=B8?= =?UTF-8?q?=EC=98=AC=EC=A7=80=20=EC=97=AC=EB=B6=80=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/parts.js | 2 +- src/services/partService.js | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/routes/parts.js b/src/routes/parts.js index 64f03d8..3de6960 100644 --- a/src/routes/parts.js +++ b/src/routes/parts.js @@ -18,7 +18,7 @@ partRouter.get( const { id } = req.params; if (!id) throw ReportableError(400, 'id 를 ì§€ì •í•´ì£¼ì„¸ìš”'); - const part = await PartService.getById(+id); + const part = await PartService.getById(+id, true); return res.sendResponse('', 200, part); }) ); diff --git a/src/services/partService.js b/src/services/partService.js index a326299..d9dbe01 100644 --- a/src/services/partService.js +++ b/src/services/partService.js @@ -5,20 +5,22 @@ const PartService = { cleanEntity(entity) { return entity; }, - async getById(id) { + async getById(id, detail = false) { if (!id || id <= 0) throw new ReportableError(400, '올바르지 ì•Šì€ id 입니다.'); const part = await PartRepository.findById(id); if (!part) throw new ReportableError(404, '해당 ë¶€í’ˆì„ ì°¾ì„ ìˆ˜ 없습니다'); - // eslint-disable-next-line no-unused-vars - const { part_id, ...infos } = await PartRepository.findMetaByTypeAndId( - part.type.toLowerCase(), - id - ); - const description = Object.values(infos).join(' / '); - part['description'] = description; + if (detail) { + // eslint-disable-next-line no-unused-vars + const { part_id, ...infos } = await PartRepository.findMetaByTypeAndId( + part.type.toLowerCase(), + id + ); + const description = Object.values(infos).join(' / '); + part['description'] = description; + } return this.cleanEntity(part); }, -- GitLab From aec0f1c1744f44bcebc333fa4c942a591ec632b0 Mon Sep 17 00:00:00 2001 From: LeeWxx <dnckd0903@naver.com> Date: Mon, 2 Dec 2024 00:12:28 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20part=20=EB=9D=BC=EC=9A=B0=ED=8A=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/parts.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/routes/parts.js b/src/routes/parts.js index 3de6960..a6a9287 100644 --- a/src/routes/parts.js +++ b/src/routes/parts.js @@ -8,7 +8,19 @@ const partRouter = Router(); partRouter.get( '/', wrapAsync(async (req, res) => { - return res.send({ message: 'parts thigns' }); + const { partType, filters } = req.query; + + const parts = await PartService.getParts(partType, filters); + + return res.sendResponse('', 200, { parts }); + }) +); + +partRouter.get( + '/filters', + wrapAsync(async (req, res) => { + const filters = await PartService.getFilters(); + return res.sendResponse('', 200, filters); }) ); -- GitLab From 193efb2f5097fc68bd7ca7c25ff5797943119daf Mon Sep 17 00:00:00 2001 From: LeeWxx <dnckd0903@naver.com> Date: Mon, 2 Dec 2024 00:15:03 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20part=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/columnMapping.js | 58 ++++++++++++++++++++++++++++++++++ src/services/partService.js | 50 +++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 src/constants/columnMapping.js diff --git a/src/constants/columnMapping.js b/src/constants/columnMapping.js new file mode 100644 index 0000000..9997dea --- /dev/null +++ b/src/constants/columnMapping.js @@ -0,0 +1,58 @@ +export const columnMapping = { + cpu: { + family_type: 'CPU 종류', + socket_type: '소켓 구분', + core_count: '코어 수', + thread_count: 'ìŠ¤ë ˆë“œ 수', + base_clock: '기본 í´ëŸ', + max_clock: '최대 í´ëŸ', + mem_type: '메모리 규격', + tdp: 'TDP', + }, + gpu: { + chipset_manufacturer: '칩셋 ì œì¡°ì‚¬', + family_type: 'ì œí’ˆ 시리즈', + chipset: '칩셋', + vram_type: '메모리 종류', + vram_size: '메모리 용량', + interface: 'ì¸í„°íŽ˜ì´ìŠ¤', + max_monitor_count: '모니터 지ì›', + power_consumption: '사용 ì „ë ¥', + }, + ram: { + usage_type: '사용 장치', + form_factor: '메모리 규격', + size: '메모리 용량', + generation: 'ì œí’ˆ 분류', + base_clock: 'ë™ìž‘ í´ëŸ(대ì—í)', + package_count: '램 개수', + }, + mb: { + board_type: 'ì œí’ˆ 분류', + cpu_socket: 'CPU 소켓', + cpu_chipset: '세부 칩셋', + power_phase: 'ì „ì›ë¶€', + ram_type: '메모리 종류', + ram_speed: '메모리 ì†ë„', + ram_slot_count: '메모리 슬롯 수', + form_factor: 'í¼íŒ©í„°', + }, + ssd: { + interface: 'ì¸í„°íŽ˜ì´ìŠ¤', + size: '용량', + form_factor: 'í¼íŒ©í„°', + nand_type: '메모리 타입', + dram_type_size: 'DRAM (ìœ í˜•ê³¼ 용량)', + protocol: 'í”„ë¡œí† ì½œ', + }, + hdd: { + usage_type: 'ì œí’ˆ 분류', + disk_standard_size: 'ë””ìŠ¤í¬ í¬ê¸°', + disk_size: 'ë””ìŠ¤í¬ ìš©ëŸ‰', + interface: 'ì¸í„°íŽ˜ì´ìŠ¤', + buffer_size: 'ë²„í¼ ìš©ëŸ‰', + rpm: 'íšŒì „ 수', + max_speed: 'ì „ì†¡ ì†ë„', + access_method: '기ë¡ë°©ì‹', + }, +}; diff --git a/src/services/partService.js b/src/services/partService.js index d9dbe01..2e8d58c 100644 --- a/src/services/partService.js +++ b/src/services/partService.js @@ -1,3 +1,4 @@ +import { columnMapping } from '../constants/columnMapping.js'; import { ReportableError } from '../errors.js'; import PartRepository from '../repositories/partRepository.js'; @@ -24,6 +25,55 @@ const PartService = { return this.cleanEntity(part); }, + async getFilters() { + const types = Object.keys(columnMapping); + const filters = {}; + + for (const type of types) { + const columns = await PartRepository.getColumnsByType(type); + + filters[type.toUpperCase()] = {}; + for (const column of columns) { + const koreanName = columnMapping[type]?.[column]; + if (koreanName) { + const filterValues = + await PartRepository.getFilterDataByTypeAndColumn(type, column); + + filters[type.toUpperCase()][koreanName] = { + column, + values: filterValues, + }; + } + } + } + + return filters; + }, + + async getParts(partType, filters) { + if (!partType) { + throw new Error('파트 íƒ€ìž…ì´ í•„ìš”í•©ë‹ˆë‹¤.'); + } + + let parsedFilters; + try { + parsedFilters = filters ? JSON.parse(filters) : {}; + } catch { + throw new ReportableError(400, 'ìž˜ëª»ëœ JSON 형태입니다.'); + } + + const whereClauses = Object.entries(parsedFilters) + .map(([key], index) => `${key} = $${index + 1}`) + .join(' AND '); + const queryValues = Object.values(parsedFilters); + + const parts = await PartRepository.getPartsByFilters( + partType, + whereClauses, + queryValues + ); + return parts; + }, }; export default PartService; -- GitLab From 7bdca9999f09e4ab2d9a7917e2fcd63f3d37d22d Mon Sep 17 00:00:00 2001 From: LeeWxx <dnckd0903@naver.com> Date: Mon, 2 Dec 2024 00:15:35 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20partRepository=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/repositories/partRepository.js | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/repositories/partRepository.js b/src/repositories/partRepository.js index 43f5c04..e5553f0 100644 --- a/src/repositories/partRepository.js +++ b/src/repositories/partRepository.js @@ -17,6 +17,45 @@ const PartRepository = { const [info] = resp.rows; return info; }, + async getColumnsByType(type) { + const resp = await pool.query( + ` + SELECT column_name + FROM information_schema.columns + WHERE table_name = $1 + ORDER BY ordinal_position; + `, + [`part_info_${type}`.toLowerCase()] + ); + return resp.rows.map((row) => row.column_name); + }, + + async getFilterDataByTypeAndColumn(type, column) { + const query = ` + SELECT DISTINCT ${column} + FROM part_info_${type} + WHERE ${column} IS NOT NULL + ORDER BY ${column} + `; + const resp = await pool.query(query); + return resp.rows.map((row) => row[column]); + }, + + async getPartsByFilters(partType, whereClauses, queryValues) { + const query = ` + SELECT + parts.id AS partId, + parts.name, + parts.image_url + FROM part_info_${partType.toLowerCase()} + INNER JOIN parts + ON part_info_${partType.toLowerCase()}.part_id = parts.id + ${whereClauses ? `WHERE ${whereClauses}` : ''} + LIMIT 20; +`; + const result = await pool.query(query, queryValues); + return result.rows; + }, }; export default PartRepository; -- GitLab From 0888fe4bf7fe4f9e1fc6b85ab79948a24dc045b0 Mon Sep 17 00:00:00 2001 From: LeeWxx <dnckd0903@naver.com> Date: Mon, 2 Dec 2024 21:36:31 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=EC=8B=A4=EC=A0=9C=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EA=B0=80=20=EA=B0=80=EC=A7=80=EA=B3=A0=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EB=B6=80=ED=92=88=EB=A7=8C=EC=9D=84=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/repositories/partRepository.js | 11 ++++++----- src/routes/parts.js | 2 -- src/services/partService.js | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/repositories/partRepository.js b/src/repositories/partRepository.js index e5553f0..4e453fa 100644 --- a/src/repositories/partRepository.js +++ b/src/repositories/partRepository.js @@ -32,11 +32,12 @@ const PartRepository = { async getFilterDataByTypeAndColumn(type, column) { const query = ` - SELECT DISTINCT ${column} - FROM part_info_${type} - WHERE ${column} IS NOT NULL - ORDER BY ${column} - `; + SELECT DISTINCT p.${column} + FROM relations r + JOIN part_info_${type} p ON r.part_id = p.part_id -- id ëŒ€ì‹ part_id 사용 + WHERE p.${column} IS NOT NULL + ORDER BY p.${column} + `; const resp = await pool.query(query); return resp.rows.map((row) => row[column]); }, diff --git a/src/routes/parts.js b/src/routes/parts.js index a6a9287..b995962 100644 --- a/src/routes/parts.js +++ b/src/routes/parts.js @@ -9,9 +9,7 @@ partRouter.get( '/', wrapAsync(async (req, res) => { const { partType, filters } = req.query; - const parts = await PartService.getParts(partType, filters); - return res.sendResponse('', 200, { parts }); }) ); diff --git a/src/services/partService.js b/src/services/partService.js index 2e8d58c..0fa797d 100644 --- a/src/services/partService.js +++ b/src/services/partService.js @@ -52,7 +52,7 @@ const PartService = { async getParts(partType, filters) { if (!partType) { - throw new Error('파트 íƒ€ìž…ì´ í•„ìš”í•©ë‹ˆë‹¤.'); + throw new ReportableError(400, '파트 íƒ€ìž…ì´ í•„ìš”í•©ë‹ˆë‹¤.'); } let parsedFilters; -- GitLab