wing-ops/backend/src/routes/layers.ts
htlee 199d5310db refactor(backend): SQLite → PostgreSQL 마이그레이션 + wing DB 연결
- better-sqlite3 제거, wingDb.ts (PostgreSQL wing DB Pool) 추가
- layers 라우터: 동기(better-sqlite3) → 비동기(pg) 전환
- LAYER 테이블 마이그레이션 SQL 생성 (database/migration/001_layer_table.sql)
- seed 스크립트 PostgreSQL 전환
- 문서 업데이트: CLAUDE.md, README.md, docs/README.md, COMMON-GUIDE.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 14:18:00 +09:00

168 lines
4.9 KiB
TypeScript
Executable File

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<Layer>(
`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<Layer>(
`SELECT ${LAYER_COLUMNS} FROM LAYER WHERE USE_YN = 'Y' ORDER BY LAYER_CD`
)
const enrichedLayers = rows.map(enrichLayerWithMetadata)
const layerMap = new Map<string, Layer & { children: Layer[] }>()
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<Layer>(
`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<Layer>(
`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<Layer>(
`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<Layer>(
`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