import { authPool } from '../db/authDb.js' import { AuthError } from '../auth/authService.js' import { type PermTreeNode, buildPermTree, type PermTreeResponse } from './permResolver.js' const PROTECTED_ROLE_CODES = ['ADMIN'] /** AUTH_PERM_TREE에서 level 0 리소스 코드를 동적 조회 */ async function getTopLevelResourceCodes(): Promise { const result = await authPool.query( `SELECT RSRC_CD FROM AUTH_PERM_TREE WHERE RSRC_LEVEL = 0 AND USE_YN = 'Y' ORDER BY SORT_ORD` ) return result.rows.map((r: { rsrc_cd: string }) => r.rsrc_cd) } /** AUTH_PERM_TREE 전체 노드 조회 */ export async function getPermTreeNodes(): Promise { const result = await authPool.query( `SELECT RSRC_CD as code, PARENT_CD as "parentCode", RSRC_NM as name, RSRC_DESC as description, ICON as icon, RSRC_LEVEL as level, SORT_ORD as "sortOrder" FROM AUTH_PERM_TREE WHERE USE_YN = 'Y' ORDER BY RSRC_LEVEL, SORT_ORD` ) return result.rows } /** 트리 구조로 변환하여 반환 (프론트엔드 UI용) */ export async function getPermTree(): Promise { const nodes = await getPermTreeNodes() return buildPermTree(nodes) } interface RoleWithPermissions { sn: number code: string name: string description: string | null isDefault: boolean permissions: Array<{ sn: number resourceCode: string operationCode: string granted: boolean }> } interface CreateRoleInput { code: string name: string description?: string } interface UpdateRoleInput { name?: string description?: string } export async function listRolesWithPermissions(): Promise { const rolesResult = await authPool.query( `SELECT ROLE_SN as sn, ROLE_CD as code, ROLE_NM as name, ROLE_DC as description, DFLT_YN as is_default FROM AUTH_ROLE ORDER BY ROLE_SN` ) const roles: RoleWithPermissions[] = [] for (const row of rolesResult.rows) { const permsResult = await authPool.query( `SELECT PERM_SN as sn, RSRC_CD as resource_code, OPER_CD as operation_code, GRANT_YN as granted FROM AUTH_PERM WHERE ROLE_SN = $1 ORDER BY RSRC_CD, OPER_CD`, [row.sn] ) roles.push({ sn: row.sn, code: row.code, name: row.name, description: row.description, isDefault: row.is_default === 'Y', permissions: permsResult.rows.map((p: { sn: number; resource_code: string; operation_code: string; granted: string }) => ({ sn: p.sn, resourceCode: p.resource_code, operationCode: p.operation_code, granted: p.granted === 'Y', })), }) } return roles } export async function updateRoleDefault(roleSn: number, isDefault: boolean): Promise { await authPool.query( 'UPDATE AUTH_ROLE SET DFLT_YN = $1 WHERE ROLE_SN = $2', [isDefault ? 'Y' : 'N', roleSn] ) } export async function createRole(input: CreateRoleInput): Promise { if (!/^[A-Z][A-Z0-9_]{0,18}[A-Z0-9]$/.test(input.code)) { throw new AuthError('역할 코드는 영문 대문자, 숫자, 언더스코어만 허용되며 2~20자입니다.', 400) } const client = await authPool.connect() try { await client.query('BEGIN') const existing = await client.query( 'SELECT 1 FROM AUTH_ROLE WHERE ROLE_CD = $1', [input.code] ) if (existing.rows.length > 0) { throw new AuthError('이미 존재하는 역할 코드입니다.', 409) } const result = await client.query( `INSERT INTO AUTH_ROLE (ROLE_CD, ROLE_NM, ROLE_DC) VALUES ($1, $2, $3) RETURNING ROLE_SN as sn, ROLE_CD as code, ROLE_NM as name, ROLE_DC as description, DFLT_YN as is_default`, [input.code, input.name, input.description || null] ) const row = result.rows[0] // 새 역할: level 0 리소스에 READ='N' 초기화 const topLevelCodes = await getTopLevelResourceCodes() for (const rsrc of topLevelCodes) { await client.query( 'INSERT INTO AUTH_PERM (ROLE_SN, RSRC_CD, OPER_CD, GRANT_YN) VALUES ($1, $2, $3, $4)', [row.sn, rsrc, 'READ', 'N'] ) } await client.query('COMMIT') const permsResult = await authPool.query( `SELECT PERM_SN as sn, RSRC_CD as resource_code, OPER_CD as operation_code, GRANT_YN as granted FROM AUTH_PERM WHERE ROLE_SN = $1 ORDER BY RSRC_CD, OPER_CD`, [row.sn] ) return { sn: row.sn, code: row.code, name: row.name, description: row.description, isDefault: row.is_default === 'Y', permissions: permsResult.rows.map((p: { sn: number; resource_code: string; operation_code: string; granted: string }) => ({ sn: p.sn, resourceCode: p.resource_code, operationCode: p.operation_code, granted: p.granted === 'Y', })), } } catch (err) { await client.query('ROLLBACK') throw err } finally { client.release() } } export async function updateRole(roleSn: number, input: UpdateRoleInput): Promise { const roleResult = await authPool.query( 'SELECT ROLE_CD FROM AUTH_ROLE WHERE ROLE_SN = $1', [roleSn] ) if (roleResult.rows.length === 0) { throw new AuthError('역할을 찾을 수 없습니다.', 404) } const sets: string[] = [] const params: (string | number | null)[] = [] let idx = 1 if (input.name !== undefined) { sets.push(`ROLE_NM = $${idx++}`) params.push(input.name) } if (input.description !== undefined) { sets.push(`ROLE_DC = $${idx++}`) params.push(input.description) } if (sets.length === 0) { throw new AuthError('수정할 항목이 없습니다.', 400) } params.push(roleSn) await authPool.query( `UPDATE AUTH_ROLE SET ${sets.join(', ')} WHERE ROLE_SN = $${idx}`, params ) } export async function deleteRole(roleSn: number): Promise { const roleResult = await authPool.query( 'SELECT ROLE_CD FROM AUTH_ROLE WHERE ROLE_SN = $1', [roleSn] ) if (roleResult.rows.length === 0) { throw new AuthError('역할을 찾을 수 없습니다.', 404) } if (PROTECTED_ROLE_CODES.includes(roleResult.rows[0].role_cd)) { throw new AuthError('시스템 보호 역할은 삭제할 수 없습니다.', 403) } await authPool.query('DELETE FROM AUTH_ROLE WHERE ROLE_SN = $1', [roleSn]) } export async function updatePermissions( roleSn: number, permissions: Array<{ resourceCode: string; operationCode: string; granted: boolean }> ): Promise { for (const perm of permissions) { const existing = await authPool.query( 'SELECT PERM_SN FROM AUTH_PERM WHERE ROLE_SN = $1 AND RSRC_CD = $2 AND OPER_CD = $3', [roleSn, perm.resourceCode, perm.operationCode] ) if (existing.rows.length > 0) { await authPool.query( 'UPDATE AUTH_PERM SET GRANT_YN = $1 WHERE ROLE_SN = $2 AND RSRC_CD = $3 AND OPER_CD = $4', [perm.granted ? 'Y' : 'N', roleSn, perm.resourceCode, perm.operationCode] ) } else { await authPool.query( 'INSERT INTO AUTH_PERM (ROLE_SN, RSRC_CD, OPER_CD, GRANT_YN) VALUES ($1, $2, $3, $4)', [roleSn, perm.resourceCode, perm.operationCode, perm.granted ? 'Y' : 'N'] ) } } }