- 메뉴 활성/비활성, 순서, 라벨, 아이콘을 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>
167 lines
5.3 KiB
TypeScript
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,
|
|
}))
|
|
}
|