import { authPool } from '../db/authDb.js' import { hashPassword, AuthError } from '../auth/authService.js' import { getSettingBoolean } from '../settings/settingsService.js' interface UserListItem { id: string account: string name: string rank: string | null orgSn: number | null orgName: string | null orgAbbr: string | null status: string failCount: number lastLogin: string | null roles: string[] roleSns: number[] regDtm: string oauthProvider: string | null email: string | null } interface CreateUserInput { account: string password: string name: string rank?: string orgSn?: number roleSns?: number[] } interface UpdateUserInput { name?: string rank?: string orgSn?: number | null status?: string } export async function listUsers(search?: string, status?: string): Promise { let query = ` SELECT u.USER_ID as id, u.USER_ACNT as account, u.USER_NM as name, u.RNKP_NM as rank, u.ORG_SN as org_sn, o.ORG_NM as org_name, o.ORG_ABBR_NM as org_abbr, u.USER_STTS_CD as status, u.FAIL_CNT as fail_count, u.LAST_LOGIN_DTM as last_login, u.REG_DTM as reg_dtm, u.OAUTH_PROVIDER as oauth_provider, u.EMAIL as email FROM AUTH_USER u LEFT JOIN AUTH_ORG o ON u.ORG_SN = o.ORG_SN WHERE 1=1 ` const params: (string | undefined)[] = [] let paramIdx = 1 if (search) { query += ` AND (u.USER_ACNT ILIKE $${paramIdx} OR u.USER_NM ILIKE $${paramIdx})` params.push(`%${search}%`) paramIdx++ } if (status) { query += ` AND u.USER_STTS_CD = $${paramIdx}` params.push(status) paramIdx++ } query += ' ORDER BY u.REG_DTM DESC' const result = await authPool.query(query, params) const users: UserListItem[] = [] for (const row of result.rows) { const rolesResult = await authPool.query( `SELECT r.ROLE_CD, r.ROLE_SN FROM AUTH_USER_ROLE ur JOIN AUTH_ROLE r ON ur.ROLE_SN = r.ROLE_SN WHERE ur.USER_ID = $1`, [row.id] ) users.push({ id: row.id, account: row.account, name: row.name, rank: row.rank, orgSn: row.org_sn, orgName: row.org_name, orgAbbr: row.org_abbr, status: row.status, failCount: row.fail_count, lastLogin: row.last_login, roles: rolesResult.rows.map((r: { role_cd: string }) => r.role_cd), roleSns: rolesResult.rows.map((r: { role_sn: number }) => r.role_sn), regDtm: row.reg_dtm, oauthProvider: row.oauth_provider, email: row.email, }) } return users } export async function getUser(userId: string): Promise { const result = await authPool.query( `SELECT u.USER_ID as id, u.USER_ACNT as account, u.USER_NM as name, u.RNKP_NM as rank, u.ORG_SN as org_sn, o.ORG_NM as org_name, o.ORG_ABBR_NM as org_abbr, u.USER_STTS_CD as status, u.FAIL_CNT as fail_count, u.LAST_LOGIN_DTM as last_login, u.REG_DTM as reg_dtm, u.OAUTH_PROVIDER as oauth_provider, u.EMAIL as email FROM AUTH_USER u LEFT JOIN AUTH_ORG o ON u.ORG_SN = o.ORG_SN WHERE u.USER_ID = $1`, [userId] ) if (result.rows.length === 0) { throw new AuthError('사용자를 찾을 수 없습니다.', 404) } const row = result.rows[0] const rolesResult = await authPool.query( `SELECT r.ROLE_CD, r.ROLE_SN FROM AUTH_USER_ROLE ur JOIN AUTH_ROLE r ON ur.ROLE_SN = r.ROLE_SN WHERE ur.USER_ID = $1`, [userId] ) return { id: row.id, account: row.account, name: row.name, rank: row.rank, orgSn: row.org_sn, orgName: row.org_name, orgAbbr: row.org_abbr, status: row.status, failCount: row.fail_count, lastLogin: row.last_login, roles: rolesResult.rows.map((r: { role_cd: string }) => r.role_cd), roleSns: rolesResult.rows.map((r: { role_sn: number }) => r.role_sn), regDtm: row.reg_dtm, oauthProvider: row.oauth_provider, email: row.email, } } export async function createUser(input: CreateUserInput): Promise<{ id: string }> { const existing = await authPool.query( 'SELECT 1 FROM AUTH_USER WHERE USER_ACNT = $1', [input.account] ) if (existing.rows.length > 0) { throw new AuthError('이미 존재하는 계정입니다.', 409) } const pswdHash = await hashPassword(input.password) // 자동 승인 설정 확인 const autoApprove = await getSettingBoolean('registration.auto-approve', true) const initialStatus = autoApprove ? 'ACTIVE' : 'PENDING' const result = await authPool.query( `INSERT INTO AUTH_USER (USER_ACNT, PSWD_HASH, USER_NM, RNKP_NM, ORG_SN, USER_STTS_CD) VALUES ($1, $2, $3, $4, $5, $6) RETURNING USER_ID as id`, [input.account, pswdHash, input.name, input.rank || null, input.orgSn || null, initialStatus] ) const userId = result.rows[0].id // 역할 할당 if (input.roleSns && input.roleSns.length > 0) { // 명시적으로 역할 지정된 경우 for (const roleSn of input.roleSns) { await authPool.query( 'INSERT INTO AUTH_USER_ROLE (USER_ID, ROLE_SN) VALUES ($1, $2)', [userId, roleSn] ) } } else { // 기본 역할 자동 할당 설정 확인 const useDefaultRole = await getSettingBoolean('registration.default-role', true) if (useDefaultRole) { const defaultRoles = await authPool.query( "SELECT ROLE_SN FROM AUTH_ROLE WHERE DFLT_YN = 'Y'" ) for (const row of defaultRoles.rows) { await authPool.query( 'INSERT INTO AUTH_USER_ROLE (USER_ID, ROLE_SN) VALUES ($1, $2)', [userId, row.role_sn] ) } } } return { id: userId } } export async function approveUser(userId: string): Promise { const result = await authPool.query( 'SELECT USER_STTS_CD FROM AUTH_USER WHERE USER_ID = $1', [userId] ) if (result.rows.length === 0) { throw new AuthError('사용자를 찾을 수 없습니다.', 404) } if (result.rows[0].user_stts_cd !== 'PENDING') { throw new AuthError('승인 대기 상태의 사용자만 승인할 수 있습니다.', 400) } await authPool.query( "UPDATE AUTH_USER SET USER_STTS_CD = 'ACTIVE', MDFCN_DTM = NOW() WHERE USER_ID = $1", [userId] ) // 기본 역할이 아직 할당되지 않았으면 할당 const existingRoles = await authPool.query( 'SELECT 1 FROM AUTH_USER_ROLE WHERE USER_ID = $1', [userId] ) if (existingRoles.rows.length === 0) { const useDefaultRole = await getSettingBoolean('registration.default-role', true) if (useDefaultRole) { const defaultRoles = await authPool.query( "SELECT ROLE_SN FROM AUTH_ROLE WHERE DFLT_YN = 'Y'" ) for (const row of defaultRoles.rows) { await authPool.query( 'INSERT INTO AUTH_USER_ROLE (USER_ID, ROLE_SN) VALUES ($1, $2)', [userId, row.role_sn] ) } } } } export async function rejectUser(userId: string): Promise { const result = await authPool.query( 'SELECT USER_STTS_CD FROM AUTH_USER WHERE USER_ID = $1', [userId] ) if (result.rows.length === 0) { throw new AuthError('사용자를 찾을 수 없습니다.', 404) } if (result.rows[0].user_stts_cd !== 'PENDING') { throw new AuthError('승인 대기 상태의 사용자만 거절할 수 있습니다.', 400) } await authPool.query( "UPDATE AUTH_USER SET USER_STTS_CD = 'REJECTED', MDFCN_DTM = NOW() WHERE USER_ID = $1", [userId] ) } export async function updateUser(userId: string, input: UpdateUserInput): Promise { const sets: string[] = [] const params: (string | number | null)[] = [] let idx = 1 if (input.name !== undefined) { sets.push(`USER_NM = $${idx++}`) params.push(input.name) } if (input.rank !== undefined) { sets.push(`RNKP_NM = $${idx++}`) params.push(input.rank) } if (input.orgSn !== undefined) { sets.push(`ORG_SN = $${idx++}`) params.push(input.orgSn) } if (input.status !== undefined) { sets.push(`USER_STTS_CD = $${idx++}`) params.push(input.status) if (input.status === 'ACTIVE') { sets.push('FAIL_CNT = 0') } } if (sets.length === 0) { throw new AuthError('수정할 항목이 없습니다.', 400) } sets.push('MDFCN_DTM = NOW()') params.push(userId) await authPool.query( `UPDATE AUTH_USER SET ${sets.join(', ')} WHERE USER_ID = $${idx}`, params ) } export async function changePassword(userId: string, newPassword: string): Promise { const pswdHash = await hashPassword(newPassword) await authPool.query( 'UPDATE AUTH_USER SET PSWD_HASH = $1, MDFCN_DTM = NOW() WHERE USER_ID = $2', [pswdHash, userId] ) } // ── 조직 목록 조회 ── interface OrgItem { orgSn: number orgNm: string orgAbbrNm: string | null orgTpCd: string upperOrgSn: number | null } export async function listOrgs(): Promise { const { rows } = await authPool.query( `SELECT ORG_SN, ORG_NM, ORG_ABBR_NM, ORG_TP_CD, UPPER_ORG_SN FROM AUTH_ORG WHERE USE_YN = 'Y' ORDER BY ORG_SN` ) return rows.map((r: Record) => ({ orgSn: r.org_sn as number, orgNm: r.org_nm as string, orgAbbrNm: r.org_abbr_nm as string | null, orgTpCd: r.org_tp_cd as string, upperOrgSn: r.upper_org_sn as number | null, })) } export async function assignRoles(userId: string, roleSns: number[]): Promise { await authPool.query('DELETE FROM AUTH_USER_ROLE WHERE USER_ID = $1', [userId]) for (const roleSn of roleSns) { await authPool.query( 'INSERT INTO AUTH_USER_ROLE (USER_ID, ROLE_SN) VALUES ($1, $2)', [userId, roleSn] ) } }