wing-ops/backend/src/settings/settingsService.ts
htlee fd48e755f2 feat(admin): 메뉴 관리 기능 구현 (DB 단일 소스, 이모지 피커)
- 메뉴 활성/비활성, 순서, 라벨, 아이콘을 DB(AUTH_SETTING)에서 관리
- GET/PUT /api/menus 엔드포인트 추가
- Zustand menuStore로 메뉴 설정 전역 상태 관리
- TopBar: DB 메뉴 설정 기반 동적 탭 렌더링 (ALL_TABS 하드코딩 제거)
- AdminView MenusPanel: API 연동, 이모지 피커(@emoji-mart) 통합
- SETTING_VAL 컬럼 VARCHAR(500) → TEXT 마이그레이션
- dotenv 추가로 .env 파일 자동 로딩
- wing_auth DB 비밀번호 기본값 수정 (JDBC 호환)

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

167 lines
5.3 KiB
TypeScript

import { authPool } from '../db/authDb.js'
interface SettingRow {
setting_key: string
setting_val: string
setting_dc: string | null
mdfcn_dtm: string
}
export interface SettingItem {
key: string
value: string
description: string | null
}
export async function getSetting(key: string): Promise<string | null> {
const result = await authPool.query(
'SELECT SETTING_VAL FROM AUTH_SETTING WHERE SETTING_KEY = $1',
[key]
)
return result.rows.length > 0 ? result.rows[0].setting_val : null
}
export async function getSettingBoolean(key: string, defaultValue = false): Promise<boolean> {
const val = await getSetting(key)
if (val === null) return defaultValue
return val === 'true'
}
export async function setSetting(key: string, value: string): Promise<void> {
await authPool.query(
`INSERT INTO AUTH_SETTING (SETTING_KEY, SETTING_VAL, MDFCN_DTM)
VALUES ($1, $2, NOW())
ON CONFLICT (SETTING_KEY) DO UPDATE SET SETTING_VAL = $2, MDFCN_DTM = NOW()`,
[key, value]
)
}
export async function getRegistrationSettings(): Promise<{
autoApprove: boolean
defaultRole: boolean
}> {
const autoApprove = await getSettingBoolean('registration.auto-approve', true)
const defaultRole = await getSettingBoolean('registration.default-role', true)
return { autoApprove, defaultRole }
}
export async function updateRegistrationSettings(settings: {
autoApprove?: boolean
defaultRole?: boolean
}): Promise<void> {
if (settings.autoApprove !== undefined) {
await setSetting('registration.auto-approve', String(settings.autoApprove))
}
if (settings.defaultRole !== undefined) {
await setSetting('registration.default-role', String(settings.defaultRole))
}
}
export async function getOAuthSettings(): Promise<{
autoApproveDomains: string
}> {
const autoApproveDomains = (await getSetting('oauth.auto-approve-domains')) || ''
return { autoApproveDomains }
}
export async function updateOAuthSettings(settings: {
autoApproveDomains?: string
}): Promise<void> {
if (settings.autoApproveDomains !== undefined) {
await setSetting('oauth.auto-approve-domains', settings.autoApproveDomains)
}
}
// ─── 메뉴 설정 ──────────────────────────────────────────────
export interface MenuConfigItem {
id: string
label: string
icon: string
enabled: boolean
order: number
}
const DEFAULT_MENU_CONFIG: MenuConfigItem[] = [
{ id: 'prediction', label: '유출유 확산예측', icon: '🛢️', enabled: true, order: 1 },
{ id: 'hns', label: 'HNS·대기확산', icon: '🧪', enabled: true, order: 2 },
{ id: 'rescue', label: '긴급구난', icon: '🚨', enabled: true, order: 3 },
{ id: 'reports', label: '보고자료', icon: '📊', enabled: true, order: 4 },
{ id: 'aerial', label: '항공탐색', icon: '✈️', enabled: true, order: 5 },
{ id: 'assets', label: '방제자산 관리', icon: '🚢', enabled: true, order: 6 },
{ id: 'scat', label: '해안평가', icon: '🏖️', enabled: true, order: 7 },
{ id: 'board', label: '게시판', icon: '📌', enabled: true, order: 8 },
{ id: 'weather', label: '기상정보', icon: '⛅', enabled: true, order: 9 },
{ id: 'incidents', label: '통합조회', icon: '🔍', enabled: true, order: 10 },
]
const VALID_MENU_IDS = DEFAULT_MENU_CONFIG.map(m => m.id)
export async function getMenuConfig(): Promise<MenuConfigItem[]> {
const val = await getSetting('menu.config')
if (!val) return DEFAULT_MENU_CONFIG
try {
const parsed = JSON.parse(val) as MenuConfigItem[]
const defaultMap = new Map(DEFAULT_MENU_CONFIG.map(m => [m.id, m]))
return parsed
.filter(item => VALID_MENU_IDS.includes(item.id))
.map(item => {
const defaults = defaultMap.get(item.id)!
return {
id: item.id,
label: item.label || defaults.label,
icon: item.icon || defaults.icon,
enabled: item.enabled,
order: item.order,
}
})
.sort((a, b) => a.order - b.order)
} catch {
return DEFAULT_MENU_CONFIG
}
}
export async function updateMenuConfig(
config: MenuConfigItem[]
): Promise<MenuConfigItem[]> {
const filtered = config.filter(item => VALID_MENU_IDS.includes(item.id))
if (filtered.length !== VALID_MENU_IDS.length) {
throw new Error('모든 메뉴 항목이 포함되어야 합니다.')
}
const ids = new Set(filtered.map(item => item.id))
if (ids.size !== filtered.length) {
throw new Error('중복된 메뉴 ID가 있습니다.')
}
const defaultMap = new Map(DEFAULT_MENU_CONFIG.map(m => [m.id, m]))
const normalized = filtered.map((item, idx) => {
const defaults = defaultMap.get(item.id)!
return {
id: item.id,
label: item.label || defaults.label,
icon: item.icon || defaults.icon,
enabled: Boolean(item.enabled),
order: idx + 1,
}
})
await setSetting('menu.config', JSON.stringify(normalized))
return normalized
}
export async function getAllSettings(): Promise<SettingItem[]> {
const result = await authPool.query<SettingRow>(
'SELECT SETTING_KEY, SETTING_VAL, SETTING_DC FROM AUTH_SETTING ORDER BY SETTING_KEY'
)
return result.rows.map(row => ({
key: row.setting_key,
value: row.setting_val,
description: row.setting_dc,
}))
}