diff --git a/src/constants/columnMapping.js b/src/constants/columnMapping.js new file mode 100644 index 0000000000000000000000000000000000000000..9997dea9bf69e56c2345401dbe8259a5ffb456a8 --- /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/repositories/partRepository.js b/src/repositories/partRepository.js new file mode 100644 index 0000000000000000000000000000000000000000..4e453fa900d1ff186583ff4448ae11f0a7531d62 --- /dev/null +++ b/src/repositories/partRepository.js @@ -0,0 +1,62 @@ +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; + }, + 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 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]); + }, + + 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; diff --git a/src/routes/parts.js b/src/routes/parts.js index 2ea1b2c894799ea8acb89325eaf1158e5c7e43a7..b995962256ae13284f72da104caa89c9ac183a32 100644 --- a/src/routes/parts.js +++ b/src/routes/parts.js @@ -1,9 +1,36 @@ 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) => { + 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); + }) +); + +partRouter.get( + '/:id', + wrapAsync(async (req, res) => { + const { id } = req.params; + if (!id) throw ReportableError(400, 'id 를 지정해주세요'); + + const part = await PartService.getById(+id, true); + return res.sendResponse('', 200, part); + }) +); export default partRouter; diff --git a/src/services/partService.js b/src/services/partService.js new file mode 100644 index 0000000000000000000000000000000000000000..0fa797d00b0ffacff33cdbc4a8364ca6643054ed --- /dev/null +++ b/src/services/partService.js @@ -0,0 +1,79 @@ +import { columnMapping } from '../constants/columnMapping.js'; +import { ReportableError } from '../errors.js'; +import PartRepository from '../repositories/partRepository.js'; + +const PartService = { + cleanEntity(entity) { + return entity; + }, + 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, '해당 부품을 찾을 수 없습니다'); + + 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); + }, + 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 ReportableError(400, '파트 타입이 필요합니다.'); + } + + 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;