import express from 'express' import { wingPool } from '../db/wingDb.js' import { enrichLayerWithMetadata } from '../utils/layerIcons.js' import { sanitizeParams, sanitizeString, isValidNumber, isValidStringLength, } from '../middleware/security.js' const router = express.Router() interface Layer { cmn_cd: string up_cmn_cd: string | null cmn_cd_full_nm: string cmn_cd_nm: string cmn_cd_level: number clnm: string | null } // DB 컬럼 → API 응답 컬럼 매핑 (프론트엔드 호환성 유지) const LAYER_COLUMNS = ` LAYER_CD AS cmn_cd, UP_LAYER_CD AS up_cmn_cd, LAYER_FULL_NM AS cmn_cd_full_nm, LAYER_NM AS cmn_cd_nm, LAYER_LEVEL AS cmn_cd_level, WMS_LAYER_NM AS clnm `.trim() // 모든 라우트에 파라미터 살균 적용 router.use(sanitizeParams) // 모든 레이어 조회 router.get('/', async (_req, res) => { try { const { rows } = await wingPool.query( `SELECT ${LAYER_COLUMNS} FROM LAYER WHERE USE_YN = 'Y' ORDER BY LAYER_CD` ) const enrichedLayers = rows.map(enrichLayerWithMetadata) res.json(enrichedLayers) } catch { res.status(500).json({ error: '레이어 조회 실패' }) } }) // 계층 구조로 변환된 레이어 트리 조회 router.get('/tree/all', async (_req, res) => { try { const { rows } = await wingPool.query( `SELECT ${LAYER_COLUMNS} FROM LAYER WHERE USE_YN = 'Y' ORDER BY LAYER_CD` ) const enrichedLayers = rows.map(enrichLayerWithMetadata) const layerMap = new Map() enrichedLayers.forEach(layer => { layerMap.set(layer.cmn_cd, { ...layer, children: [] }) }) const rootLayers: (Layer & { children: Layer[] })[] = [] enrichedLayers.forEach(layer => { const layerNode = layerMap.get(layer.cmn_cd)! if (layer.up_cmn_cd === null) { rootLayers.push(layerNode) } else { const parent = layerMap.get(layer.up_cmn_cd) if (parent) { parent.children.push(layerNode) } } }) res.json(rootLayers) } catch { res.status(500).json({ error: '레이어 트리 조회 실패' }) } }) // WMS 레이어만 조회 router.get('/wms/all', async (_req, res) => { try { const { rows } = await wingPool.query( `SELECT ${LAYER_COLUMNS} FROM LAYER WHERE WMS_LAYER_NM IS NOT NULL AND USE_YN = 'Y' ORDER BY LAYER_CD` ) const enrichedLayers = rows.map(enrichLayerWithMetadata) res.json(enrichedLayers) } catch { res.status(500).json({ error: 'WMS 레이어 조회 실패' }) } }) // 특정 레벨의 레이어만 조회 router.get('/level/:level', async (req, res) => { try { const level = parseInt(req.params.level, 10) if (!isValidNumber(level, 1, 10)) { return res.status(400).json({ error: '유효하지 않은 레벨값', message: '레벨은 1~10 범위의 정수여야 합니다.' }) } const { rows } = await wingPool.query( `SELECT ${LAYER_COLUMNS} FROM LAYER WHERE LAYER_LEVEL = $1 AND USE_YN = 'Y' ORDER BY LAYER_CD`, [level] ) const enrichedLayers = rows.map(enrichLayerWithMetadata) res.json(enrichedLayers) } catch { res.status(500).json({ error: '레벨별 레이어 조회 실패' }) } }) // 특정 부모의 자식 레이어 조회 router.get('/children/:parentId', async (req, res) => { try { const parentId = req.params.parentId if (!parentId || !isValidStringLength(parentId, 50) || !/^[a-zA-Z0-9_-]+$/.test(parentId)) { return res.status(400).json({ error: '유효하지 않은 부모 ID', message: 'ID는 영숫자, 언더스코어, 하이픈만 허용됩니다.' }) } const sanitizedId = sanitizeString(parentId) const { rows } = await wingPool.query( `SELECT ${LAYER_COLUMNS} FROM LAYER WHERE UP_LAYER_CD = $1 AND USE_YN = 'Y' ORDER BY LAYER_CD`, [sanitizedId] ) const enrichedLayers = rows.map(enrichLayerWithMetadata) res.json(enrichedLayers) } catch { res.status(500).json({ error: '자식 레이어 조회 실패' }) } }) // 특정 레이어 조회 router.get('/:id', async (req, res) => { try { const id = req.params.id if (!id || !isValidStringLength(id, 50) || !/^[a-zA-Z0-9_-]+$/.test(id)) { return res.status(400).json({ error: '유효하지 않은 레이어 ID', message: 'ID는 영숫자, 언더스코어, 하이픈만 허용됩니다.' }) } const sanitizedId = sanitizeString(id) const { rows } = await wingPool.query( `SELECT ${LAYER_COLUMNS} FROM LAYER WHERE LAYER_CD = $1`, [sanitizedId] ) if (rows.length === 0) { return res.status(404).json({ error: '레이어를 찾을 수 없습니다' }) } const enrichedLayer = enrichLayerWithMetadata(rows[0]) res.json(enrichedLayer) } catch { res.status(500).json({ error: '레이어 조회 실패' }) } }) export default router