관리자와 회원 설정 계정 작업 정리

This commit is contained in:
2026-05-13 15:26:26 +09:00
parent 6481f958f5
commit bebf7ee1c9
18 changed files with 811 additions and 175 deletions

View File

@@ -27,6 +27,7 @@ const getMemberRoleLabel = (roleCode) => roleCode === MEMBER_ROLE.OWNER
*/
const mapAdminMemberRow = (row) => {
const lastSeenAt = row.lastSeenAt ? row.lastSeenAt.toISOString() : null
const previousLastSeenAt = row.previousLastSeenAt ? row.previousLastSeenAt.toISOString() : null
const isActive = row.lastSeenAt
? Date.now() - new Date(row.lastSeenAt).getTime() <= 1000 * 60 * 60 * 24 * 30
: false
@@ -45,6 +46,8 @@ const mapAdminMemberRow = (row) => {
updatedAt: row.updatedAt.toISOString(),
lastSeenAt,
lastSeenIp: row.lastSeenIp || '',
previousLastSeenAt,
previousLastSeenIp: row.previousLastSeenIp || '',
commentCount: Number(row.commentCount || 0),
activityStatus: isActive ? '활성' : '비활성',
role: getMemberRoleLabel(roleCode)
@@ -64,6 +67,8 @@ const mapAdminMemberRow = (row) => {
* @property {string} updatedAt - 수정 시각(ISO)
* @property {string | null} lastSeenAt - 최근 접속 시각(ISO)
* @property {string} lastSeenIp - 최근 접속 IP
* @property {string | null} previousLastSeenAt - 이전 로그인 시각(ISO)
* @property {string} previousLastSeenIp - 이전 로그인 IP
*/
/**
@@ -100,7 +105,9 @@ export const getUserByEmail = async (email) => {
created_at AS "createdAt",
updated_at AS "updatedAt",
last_seen_at AS "lastSeenAt",
last_seen_ip AS "lastSeenIp"
last_seen_ip AS "lastSeenIp",
previous_last_seen_at AS "previousLastSeenAt",
previous_last_seen_ip AS "previousLastSeenIp"
FROM users
WHERE lower(email) = lower(${email})
LIMIT 1
@@ -127,7 +134,9 @@ export const getUserById = async (id) => {
created_at AS "createdAt",
updated_at AS "updatedAt",
last_seen_at AS "lastSeenAt",
last_seen_ip AS "lastSeenIp"
last_seen_ip AS "lastSeenIp",
previous_last_seen_at AS "previousLastSeenAt",
previous_last_seen_ip AS "previousLastSeenIp"
FROM users
WHERE id = ${id}
LIMIT 1
@@ -136,6 +145,38 @@ export const getUserById = async (id) => {
return rows?.[0] || null
}
/**
* 사용자 설정 화면용 회원 프로필을 조회한다.
* @param {string} id - 사용자 ID
* @returns {Promise<(Omit<MemberUser, 'passwordHash'> & { commentCount: number }) | null>} 회원 프로필
*/
export const getUserProfileById = async (id) => {
const sql = requireSql()
const rows = await sql`
SELECT
users.id,
users.username,
users.email,
users.avatar_url AS "avatarUrl",
users.is_admin AS "isAdmin",
users.user_role AS "role",
users.created_at AS "createdAt",
users.updated_at AS "updatedAt",
users.last_seen_at AS "lastSeenAt",
users.last_seen_ip AS "lastSeenIp",
users.previous_last_seen_at AS "previousLastSeenAt",
users.previous_last_seen_ip AS "previousLastSeenIp",
COALESCE(count(comments.id), 0)::int AS "commentCount"
FROM users
LEFT JOIN comments ON comments.user_id = users.id AND comments.status = 'published'
WHERE users.id = ${id}
GROUP BY users.id
LIMIT 1
`
return rows?.[0] || null
}
/**
* ID로 회원 조회(비밀번호 포함)
* @param {string} id - 사용자 ID
@@ -155,7 +196,9 @@ export const getUserByIdWithPassword = async (id) => {
created_at AS "createdAt",
updated_at AS "updatedAt",
last_seen_at AS "lastSeenAt",
last_seen_ip AS "lastSeenIp"
last_seen_ip AS "lastSeenIp",
previous_last_seen_at AS "previousLastSeenAt",
previous_last_seen_ip AS "previousLastSeenIp"
FROM users
WHERE id = ${id}
LIMIT 1
@@ -198,7 +241,9 @@ export const createUser = async (input) => {
created_at AS "createdAt",
updated_at AS "updatedAt",
last_seen_at AS "lastSeenAt",
last_seen_ip AS "lastSeenIp"
last_seen_ip AS "lastSeenIp",
previous_last_seen_at AS "previousLastSeenAt",
previous_last_seen_ip AS "previousLastSeenIp"
`
})
@@ -223,6 +268,8 @@ export const touchUserActivity = async (input) => {
await sql`
UPDATE users
SET
previous_last_seen_at = last_seen_at,
previous_last_seen_ip = last_seen_ip,
last_seen_at = now(),
last_seen_ip = ${input.ip},
updated_at = now()
@@ -254,7 +301,9 @@ export const updateMemberProfile = async (input) => {
created_at AS "createdAt",
updated_at AS "updatedAt",
last_seen_at AS "lastSeenAt",
last_seen_ip AS "lastSeenIp"
last_seen_ip AS "lastSeenIp",
previous_last_seen_at AS "previousLastSeenAt",
previous_last_seen_ip AS "previousLastSeenIp"
`
return rows?.[0] || null
@@ -308,6 +357,38 @@ export const deleteMember = async (userId) => {
`
}
/**
* 관리자 화면에서 회원을 삭제한다.
* @param {{ actorUserId: string, targetUserId: string }} input - 삭제 정보
* @returns {Promise<Object>} 삭제된 회원
*/
export const deleteMemberByAdmin = async (input) => {
const target = await getMemberForAdmin(input.targetUserId)
if (!target) {
throw createError({
statusCode: 404,
message: '회원을 찾을 수 없습니다.'
})
}
if (target.id === input.actorUserId) {
throw createError({
statusCode: 400,
message: '현재 로그인한 계정은 여기서 삭제할 수 없습니다.'
})
}
if (target.roleCode === MEMBER_ROLE.OWNER && (await countOwnerMembers()) <= 1) {
throw createError({
statusCode: 400,
message: '최소 1명의 소유자 권한은 유지되어야 합니다.'
})
}
await deleteMember(input.targetUserId)
return target
}
/**
* 소유자 권한 회원 수를 조회한다.
* @returns {Promise<number>} 소유자 회원 수
@@ -375,7 +456,7 @@ export const isEmailTaken = async (input) => {
/**
* 관리자용 회원 목록 조회(댓글 활동 포함)
* @returns {Promise<Array<{ id: string, username: string, email: string, avatarUrl: string, isAdmin: boolean, roleCode: string, createdAt: string, updatedAt: string, lastSeenAt: string | null, lastSeenIp: string, commentCount: number, activityStatus: string, role: string }>>} 회원 목록
* @returns {Promise<Array<{ id: string, username: string, email: string, avatarUrl: string, isAdmin: boolean, roleCode: string, createdAt: string, updatedAt: string, lastSeenAt: string | null, lastSeenIp: string, previousLastSeenAt: string | null, previousLastSeenIp: string, commentCount: number, activityStatus: string, role: string }>>} 회원 목록
*/
export const listMembersForAdmin = async () => {
const sql = requireSql()
@@ -393,6 +474,8 @@ export const listMembersForAdmin = async () => {
users.updated_at AS "updatedAt",
users.last_seen_at AS "lastSeenAt",
users.last_seen_ip AS "lastSeenIp",
users.previous_last_seen_at AS "previousLastSeenAt",
users.previous_last_seen_ip AS "previousLastSeenIp",
COALESCE(count(comments.id), 0)::int AS "commentCount"
FROM users
LEFT JOIN comments ON comments.user_id = users.id AND comments.status = 'published'
@@ -424,6 +507,8 @@ export const getMemberForAdmin = async (memberId) => {
users.updated_at AS "updatedAt",
users.last_seen_at AS "lastSeenAt",
users.last_seen_ip AS "lastSeenIp",
users.previous_last_seen_at AS "previousLastSeenAt",
users.previous_last_seen_ip AS "previousLastSeenIp",
COALESCE(count(comments.id), 0)::int AS "commentCount"
FROM users
LEFT JOIN comments ON comments.user_id = users.id AND comments.status = 'published'
@@ -519,7 +604,9 @@ export const getAdminUserByEmail = async (email) => {
created_at AS "createdAt",
updated_at AS "updatedAt",
last_seen_at AS "lastSeenAt",
last_seen_ip AS "lastSeenIp"
last_seen_ip AS "lastSeenIp",
previous_last_seen_at AS "previousLastSeenAt",
previous_last_seen_ip AS "previousLastSeenIp"
FROM users
WHERE lower(email) = lower(${email})
AND user_role = ANY(${PRIVILEGED_ROLES})