- 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>
168 lines
4.9 KiB
TypeScript
Executable File
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
|