From ef7914d5c6d564ac85e0e9e7d62faf003745fd40 Mon Sep 17 00:00:00 2001 From: Hyoseong Jo Date: Sat, 10 Jan 2026 16:54:06 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B6=8C=ED=95=9C,=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90,=20=EB=A9=94=EB=89=B4=20=EB=93=B1=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=EA=B8=B0=EB=8A=A5=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/vcs.xml | 1 + .../api/admin/menu/[id]/toggle-role.post.ts | 44 +++ backend/api/admin/menu/list.get.ts | 73 ++++ backend/api/admin/role/[id]/delete.delete.ts | 46 +++ backend/api/admin/role/[id]/update.put.ts | 54 +++ backend/api/admin/role/create.post.ts | 45 +++ backend/api/admin/role/list.get.ts | 34 ++ backend/api/admin/user/[id]/roles.put.ts | 55 +++ .../api/admin/user/[id]/toggle-role.post.ts | 70 ++++ backend/api/admin/user/list.get.ts | 110 ++++++ backend/api/auth/current-user.get.ts | 11 +- backend/api/auth/me.get.ts | 12 +- backend/api/auth/menu.get.ts | 80 ++++ backend/api/employee/[id]/delete.delete.ts | 26 +- backend/api/employee/[id]/detail.get.ts | 4 +- backend/sql/create_role_tables.sql | 59 +++ backend/utils/session.ts | 52 +++ claude_temp/TASK_MENU_PERMISSION_20260110.md | 267 +++++++++++++ claude_temp/TASK_ROLE_MANAGEMENT.md | 123 ++++++ claude_temp/TASK_USER_CRUD.md | 81 ++++ frontend/admin/bulk-import.vue | 8 +- frontend/admin/menu/index.vue | 161 ++++++++ frontend/admin/user/[id].vue | 359 ++++++++++++++++++ frontend/admin/user/create.vue | 177 +++++++++ frontend/admin/user/index.vue | 352 +++++++++++++++++ .../components/common/RoleManageModal.vue | 231 +++++++++++ frontend/components/layout/AppHeader.vue | 72 ++-- frontend/composables/useAuth.ts | 92 ++++- frontend/employee/[id].vue | 241 ------------ frontend/employee/index.vue | 320 ---------------- frontend/mypage/index.vue | 50 ++- frontend/report/summary/index.vue | 8 +- frontend/report/weekly/[id].vue | 4 +- frontend/report/weekly/index.vue | 6 +- 34 files changed, 2678 insertions(+), 650 deletions(-) create mode 100644 backend/api/admin/menu/[id]/toggle-role.post.ts create mode 100644 backend/api/admin/menu/list.get.ts create mode 100644 backend/api/admin/role/[id]/delete.delete.ts create mode 100644 backend/api/admin/role/[id]/update.put.ts create mode 100644 backend/api/admin/role/create.post.ts create mode 100644 backend/api/admin/role/list.get.ts create mode 100644 backend/api/admin/user/[id]/roles.put.ts create mode 100644 backend/api/admin/user/[id]/toggle-role.post.ts create mode 100644 backend/api/admin/user/list.get.ts create mode 100644 backend/api/auth/menu.get.ts create mode 100644 backend/sql/create_role_tables.sql create mode 100644 claude_temp/TASK_MENU_PERMISSION_20260110.md create mode 100644 claude_temp/TASK_ROLE_MANAGEMENT.md create mode 100644 claude_temp/TASK_USER_CRUD.md create mode 100644 frontend/admin/menu/index.vue create mode 100644 frontend/admin/user/[id].vue create mode 100644 frontend/admin/user/create.vue create mode 100644 frontend/admin/user/index.vue create mode 100644 frontend/components/common/RoleManageModal.vue delete mode 100644 frontend/employee/[id].vue delete mode 100644 frontend/employee/index.vue diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..8306744 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/backend/api/admin/menu/[id]/toggle-role.post.ts b/backend/api/admin/menu/[id]/toggle-role.post.ts new file mode 100644 index 0000000..2c0689d --- /dev/null +++ b/backend/api/admin/menu/[id]/toggle-role.post.ts @@ -0,0 +1,44 @@ +import { queryOne, execute } from '../../../../utils/db' +import { requireAdmin } from '../../../../utils/session' + +/** + * 메뉴 권한 토글 + * POST /api/admin/menu/[id]/toggle-role + */ +export default defineEventHandler(async (event) => { + await requireAdmin(event) + + const menuId = getRouterParam(event, 'id') + const body = await readBody(event) + const { roleId, enabled } = body + + if (!roleId) { + throw createError({ statusCode: 400, message: '권한 ID가 필요합니다.' }) + } + + // 메뉴 존재 확인 + const menu = await queryOne(` + SELECT menu_id FROM wr_menu WHERE menu_id = $1 + `, [menuId]) + + if (!menu) { + throw createError({ statusCode: 404, message: '메뉴를 찾을 수 없습니다.' }) + } + + if (enabled) { + // 권한 추가 + await execute(` + INSERT INTO wr_menu_role (menu_id, role_id) + VALUES ($1, $2) + ON CONFLICT (menu_id, role_id) DO NOTHING + `, [menuId, roleId]) + } else { + // 권한 제거 + await execute(` + DELETE FROM wr_menu_role + WHERE menu_id = $1 AND role_id = $2 + `, [menuId, roleId]) + } + + return { success: true } +}) diff --git a/backend/api/admin/menu/list.get.ts b/backend/api/admin/menu/list.get.ts new file mode 100644 index 0000000..0538d64 --- /dev/null +++ b/backend/api/admin/menu/list.get.ts @@ -0,0 +1,73 @@ +import { query } from '../../../utils/db' +import { requireAdmin } from '../../../utils/session' + +/** + * 메뉴 목록 조회 (권한 포함) + * GET /api/admin/menu/list + */ +export default defineEventHandler(async (event) => { + await requireAdmin(event) + + // 메뉴 목록 조회 + const menus = await query(` + SELECT + m.menu_id, + m.menu_code, + m.menu_name, + m.menu_path, + m.menu_icon, + m.parent_menu_id, + m.sort_order, + m.is_active, + m.created_at, + m.updated_at, + pm.menu_name AS parent_menu_name + FROM wr_menu m + LEFT JOIN wr_menu pm ON m.parent_menu_id = pm.menu_id + ORDER BY m.parent_menu_id NULLS FIRST, m.sort_order + `) + + // 권한 목록 조회 + const roles = await query(` + SELECT role_id, role_code, role_name + FROM wr_role + ORDER BY role_id + `) + + // 메뉴-권한 매핑 조회 + const menuRoles = await query(` + SELECT menu_id, role_id + FROM wr_menu_role + `) + + // 메뉴별 권한 매핑 정리 + const menuRoleMap: Record = {} + for (const mr of menuRoles) { + if (!menuRoleMap[mr.menu_id]) { + menuRoleMap[mr.menu_id] = [] + } + menuRoleMap[mr.menu_id].push(mr.role_id) + } + + return { + menus: menus.map(m => ({ + menuId: m.menu_id, + menuCode: m.menu_code, + menuName: m.menu_name, + menuPath: m.menu_path, + menuIcon: m.menu_icon, + parentMenuId: m.parent_menu_id, + parentMenuName: m.parent_menu_name, + sortOrder: m.sort_order, + isActive: m.is_active, + createdAt: m.created_at, + updatedAt: m.updated_at, + roleIds: menuRoleMap[m.menu_id] || [] + })), + roles: roles.map((r: any) => ({ + roleId: r.role_id, + roleCode: r.role_code, + roleName: r.role_name + })) + } +}) diff --git a/backend/api/admin/role/[id]/delete.delete.ts b/backend/api/admin/role/[id]/delete.delete.ts new file mode 100644 index 0000000..01913c4 --- /dev/null +++ b/backend/api/admin/role/[id]/delete.delete.ts @@ -0,0 +1,46 @@ +import { queryOne, execute } from '../../../../utils/db' +import { requireAdmin } from '../../../../utils/session' + +/** + * 권한 삭제 + * DELETE /api/admin/role/[id]/delete + */ +export default defineEventHandler(async (event) => { + await requireAdmin(event) + + const roleId = getRouterParam(event, 'id') + if (!roleId) { + throw createError({ statusCode: 400, message: '권한 ID가 필요합니다.' }) + } + + // 존재 여부 확인 + const existing = await queryOne(` + SELECT role_id, role_code FROM wr_role WHERE role_id = $1 + `, [roleId]) + + if (!existing) { + throw createError({ statusCode: 404, message: '권한을 찾을 수 없습니다.' }) + } + + // 기본 권한은 삭제 불가 + const protectedCodes = ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_USER'] + if (protectedCodes.includes(existing.role_code)) { + throw createError({ statusCode: 400, message: '기본 권한은 삭제할 수 없습니다.' }) + } + + // 사용 중인 권한인지 확인 + const usageCount = await queryOne(` + SELECT COUNT(*) as cnt FROM wr_employee_role WHERE role_id = $1 + `, [roleId]) + + if (parseInt(usageCount.cnt) > 0) { + throw createError({ + statusCode: 400, + message: `${usageCount.cnt}명의 사용자가 이 권한을 사용 중입니다. 먼저 권한을 해제해주세요.` + }) + } + + await execute(`DELETE FROM wr_role WHERE role_id = $1`, [roleId]) + + return { success: true } +}) diff --git a/backend/api/admin/role/[id]/update.put.ts b/backend/api/admin/role/[id]/update.put.ts new file mode 100644 index 0000000..a86c80f --- /dev/null +++ b/backend/api/admin/role/[id]/update.put.ts @@ -0,0 +1,54 @@ +import { queryOne, execute } from '../../../../utils/db' +import { requireAdmin } from '../../../../utils/session' + +/** + * 권한 수정 + * PUT /api/admin/role/[id]/update + */ +export default defineEventHandler(async (event) => { + await requireAdmin(event) + + const roleId = getRouterParam(event, 'id') + if (!roleId) { + throw createError({ statusCode: 400, message: '권한 ID가 필요합니다.' }) + } + + const body = await readBody<{ + roleName?: string + roleDescription?: string + isInternalIpOnly?: boolean + sortOrder?: number + isActive?: boolean + }>(event) + + // 존재 여부 확인 + const existing = await queryOne(` + SELECT role_id, role_code FROM wr_role WHERE role_id = $1 + `, [roleId]) + + if (!existing) { + throw createError({ statusCode: 404, message: '권한을 찾을 수 없습니다.' }) + } + + await execute(` + UPDATE wr_role SET + role_name = COALESCE($2, role_name), + role_description = COALESCE($3, role_description), + is_internal_ip_only = COALESCE($4, is_internal_ip_only), + sort_order = COALESCE($5, sort_order), + is_active = COALESCE($6, is_active), + updated_at = NOW() + WHERE role_id = $1 + `, [ + roleId, + body.roleName, + body.roleDescription, + body.isInternalIpOnly, + body.sortOrder, + body.isActive + ]) + + const updated = await queryOne(`SELECT * FROM wr_role WHERE role_id = $1`, [roleId]) + + return { success: true, role: updated } +}) diff --git a/backend/api/admin/role/create.post.ts b/backend/api/admin/role/create.post.ts new file mode 100644 index 0000000..612cde8 --- /dev/null +++ b/backend/api/admin/role/create.post.ts @@ -0,0 +1,45 @@ +import { queryOne } from '../../../utils/db' +import { requireAdmin } from '../../../utils/session' + +/** + * 권한 생성 + * POST /api/admin/role/create + */ +export default defineEventHandler(async (event) => { + await requireAdmin(event) + + const body = await readBody<{ + roleCode: string + roleName: string + roleDescription?: string + isInternalIpOnly?: boolean + sortOrder?: number + }>(event) + + if (!body.roleCode || !body.roleName) { + throw createError({ statusCode: 400, message: '권한코드와 권한명은 필수입니다.' }) + } + + // 코드 중복 체크 + const existing = await queryOne(` + SELECT role_id FROM wr_role WHERE role_code = $1 + `, [body.roleCode]) + + if (existing) { + throw createError({ statusCode: 400, message: '이미 존재하는 권한코드입니다.' }) + } + + const role = await queryOne(` + INSERT INTO wr_role (role_code, role_name, role_description, is_internal_ip_only, sort_order) + VALUES ($1, $2, $3, $4, $5) + RETURNING * + `, [ + body.roleCode, + body.roleName, + body.roleDescription || null, + body.isInternalIpOnly || false, + body.sortOrder || 0 + ]) + + return { success: true, role } +}) diff --git a/backend/api/admin/role/list.get.ts b/backend/api/admin/role/list.get.ts new file mode 100644 index 0000000..39187e7 --- /dev/null +++ b/backend/api/admin/role/list.get.ts @@ -0,0 +1,34 @@ +import { query } from '../../../utils/db' +import { requireAdmin } from '../../../utils/session' + +/** + * 권한 목록 조회 + * GET /api/admin/role/list + */ +export default defineEventHandler(async (event) => { + // 관리자 권한 체크 + await requireAdmin(event) + + const roles = await query(` + SELECT + r.role_id, + r.role_code, + r.role_name, + r.role_description, + r.is_internal_ip_only, + r.sort_order, + r.is_active, + r.created_at, + r.updated_at, + COUNT(DISTINCT er.employee_id) as user_count + FROM wr_role r + LEFT JOIN wr_employee_role er ON r.role_id = er.role_id + GROUP BY r.role_id + ORDER BY r.sort_order, r.role_id + `) + + return { + roles, + total: roles.length + } +}) diff --git a/backend/api/admin/user/[id]/roles.put.ts b/backend/api/admin/user/[id]/roles.put.ts new file mode 100644 index 0000000..936c340 --- /dev/null +++ b/backend/api/admin/user/[id]/roles.put.ts @@ -0,0 +1,55 @@ +import { query, queryOne, execute } from '../../../../utils/db' +import { requireAdmin } from '../../../../utils/session' + +/** + * 사용자 권한 변경 + * PUT /api/admin/user/[id]/roles + * + * Body: { roleIds: number[] } + */ +export default defineEventHandler(async (event) => { + await requireAdmin(event) + + const employeeId = getRouterParam(event, 'id') + if (!employeeId) { + throw createError({ statusCode: 400, message: '사용자 ID가 필요합니다.' }) + } + + const body = await readBody<{ roleIds: number[] }>(event) + const roleIds = body.roleIds || [] + + // 사용자 존재 확인 + const user = await queryOne(` + SELECT employee_id, employee_email FROM wr_employee_info WHERE employee_id = $1 + `, [employeeId]) + + if (!user) { + throw createError({ statusCode: 404, message: '사용자를 찾을 수 없습니다.' }) + } + + // 기존 권한 모두 삭제 + await execute(`DELETE FROM wr_employee_role WHERE employee_id = $1`, [employeeId]) + + // 새 권한 추가 + for (const roleId of roleIds) { + await execute(` + INSERT INTO wr_employee_role (employee_id, role_id) + VALUES ($1, $2) + ON CONFLICT (employee_id, role_id) DO NOTHING + `, [employeeId, roleId]) + } + + // 변경된 권한 조회 + const updatedRoles = await query(` + SELECT r.role_id, r.role_code, r.role_name + FROM wr_employee_role er + JOIN wr_role r ON er.role_id = r.role_id + WHERE er.employee_id = $1 + `, [employeeId]) + + return { + success: true, + employeeId: parseInt(employeeId as string), + roles: updatedRoles + } +}) diff --git a/backend/api/admin/user/[id]/toggle-role.post.ts b/backend/api/admin/user/[id]/toggle-role.post.ts new file mode 100644 index 0000000..5afc124 --- /dev/null +++ b/backend/api/admin/user/[id]/toggle-role.post.ts @@ -0,0 +1,70 @@ +import { queryOne, execute } from '../../../../utils/db' +import { requireAdmin } from '../../../../utils/session' + +/** + * 사용자 개별 권한 토글 (추가/제거) + * POST /api/admin/user/[id]/toggle-role + * + * Body: { roleId: number } + */ +export default defineEventHandler(async (event) => { + await requireAdmin(event) + + const employeeId = getRouterParam(event, 'id') + if (!employeeId) { + throw createError({ statusCode: 400, message: '사용자 ID가 필요합니다.' }) + } + + const body = await readBody<{ roleId: number }>(event) + if (!body.roleId) { + throw createError({ statusCode: 400, message: '권한 ID가 필요합니다.' }) + } + + // 사용자 존재 확인 + const user = await queryOne(` + SELECT employee_id FROM wr_employee_info WHERE employee_id = $1 + `, [employeeId]) + + if (!user) { + throw createError({ statusCode: 404, message: '사용자를 찾을 수 없습니다.' }) + } + + // 권한 존재 확인 + const role = await queryOne(` + SELECT role_id, role_code FROM wr_role WHERE role_id = $1 + `, [body.roleId]) + + if (!role) { + throw createError({ statusCode: 404, message: '권한을 찾을 수 없습니다.' }) + } + + // 현재 권한 보유 여부 확인 + const existing = await queryOne(` + SELECT employee_role_id FROM wr_employee_role + WHERE employee_id = $1 AND role_id = $2 + `, [employeeId, body.roleId]) + + let added: boolean + + if (existing) { + // 권한 제거 + await execute(` + DELETE FROM wr_employee_role WHERE employee_id = $1 AND role_id = $2 + `, [employeeId, body.roleId]) + added = false + } else { + // 권한 추가 + await execute(` + INSERT INTO wr_employee_role (employee_id, role_id) VALUES ($1, $2) + `, [employeeId, body.roleId]) + added = true + } + + return { + success: true, + employeeId: parseInt(employeeId as string), + roleId: body.roleId, + roleCode: role.role_code, + added + } +}) diff --git a/backend/api/admin/user/list.get.ts b/backend/api/admin/user/list.get.ts new file mode 100644 index 0000000..78fb4ff --- /dev/null +++ b/backend/api/admin/user/list.get.ts @@ -0,0 +1,110 @@ +import { query } from '../../../utils/db' +import { requireAdmin } from '../../../utils/session' + +/** + * 사용자 목록 조회 (권한 정보 + 최근 로그인 포함) + * GET /api/admin/user/list + */ +export default defineEventHandler(async (event) => { + await requireAdmin(event) + + const queryParams = getQuery(event) + const company = queryParams.company as string || '' + const name = queryParams.name as string || '' + const email = queryParams.email as string || '' + const phone = queryParams.phone as string || '' + const status = queryParams.status as string || 'active' // 기본값: 활성 + + // 1. 사용자 목록 조회 (최근 로그인 포함) + let userQuery = ` + SELECT + e.employee_id, + e.employee_name, + e.employee_email, + e.employee_phone, + e.employee_position, + e.company, + e.join_date, + e.is_active, + e.created_at, + ( + SELECT MAX(login_at) + FROM wr_login_history + WHERE employee_id = e.employee_id + ) as last_login_at + FROM wr_employee_info e + WHERE 1=1 + ` + const params: any[] = [] + + // 소속사 검색 + if (company) { + params.push(`%${company}%`) + userQuery += ` AND e.company ILIKE $${params.length}` + } + + // 이름 검색 + if (name) { + params.push(`%${name}%`) + userQuery += ` AND e.employee_name ILIKE $${params.length}` + } + + // 이메일 검색 + if (email) { + params.push(`%${email}%`) + userQuery += ` AND e.employee_email ILIKE $${params.length}` + } + + // 전화번호 검색 + if (phone) { + params.push(`%${phone}%`) + userQuery += ` AND e.employee_phone ILIKE $${params.length}` + } + + // 상태 검색 + if (status === 'active') { + userQuery += ` AND e.is_active = true` + } else if (status === 'inactive') { + userQuery += ` AND e.is_active = false` + } + // status === 'all' 이면 조건 없음 + + userQuery += ` ORDER BY e.company, e.employee_position, e.employee_name` + + const users = await query(userQuery, params) + + // 2. 모든 권한 목록 조회 + const roles = await query(` + SELECT role_id, role_code, role_name, sort_order + FROM wr_role + WHERE is_active = true + ORDER BY sort_order + `) + + // 3. 사용자별 권한 매핑 조회 + const userRoles = await query(` + SELECT employee_id, role_id + FROM wr_employee_role + `) + + // 4. 사용자별 권한 배열 생성 + const userRoleMap = new Map() + for (const ur of userRoles) { + if (!userRoleMap.has(ur.employee_id)) { + userRoleMap.set(ur.employee_id, []) + } + userRoleMap.get(ur.employee_id)!.push(ur.role_id) + } + + // 5. 사용자 데이터에 권한 정보 추가 + const usersWithRoles = users.map(u => ({ + ...u, + roleIds: userRoleMap.get(u.employee_id) || [] + })) + + return { + users: usersWithRoles, + roles, + total: users.length + } +}) diff --git a/backend/api/auth/current-user.get.ts b/backend/api/auth/current-user.get.ts index fd552a7..b33a73e 100644 --- a/backend/api/auth/current-user.get.ts +++ b/backend/api/auth/current-user.get.ts @@ -1,7 +1,8 @@ -import { getSession, refreshSession, getSessionIdFromCookie, deleteSessionCookie } from '../../utils/session' +import { getDbSession, refreshSession, getSessionIdFromCookie, deleteSessionCookie, getUserRoles } from '../../utils/session' +import { queryOne, execute, query } from '../../utils/db' /** - * 현재 로그인 사용자 정보 + * 현재 로그인 사용자 정보 (권한 포함) * GET /api/auth/current-user */ export default defineEventHandler(async (event) => { @@ -43,12 +44,16 @@ export default defineEventHandler(async (event) => { `, [session.loginHistoryId]) } + // 사용자 권한 조회 + const roles = await getUserRoles(employee.employee_id) + return { user: { employeeId: employee.employee_id, employeeName: employee.employee_name, employeeEmail: employee.employee_email, - employeePosition: employee.employee_position + employeePosition: employee.employee_position, + roles // 권한 코드 배열 추가 } } }) diff --git a/backend/api/auth/me.get.ts b/backend/api/auth/me.get.ts index 9bc7372..a99024d 100644 --- a/backend/api/auth/me.get.ts +++ b/backend/api/auth/me.get.ts @@ -28,7 +28,11 @@ export default defineEventHandler(async (event) => { employee_position, company, join_date, - is_active + is_active, + created_at, + created_ip, + updated_at, + updated_ip FROM wr_employee_info WHERE employee_id = $1 `, [session.employeeId]) @@ -46,7 +50,11 @@ export default defineEventHandler(async (event) => { employeePosition: employee.employee_position, company: employee.company, joinDate: employee.join_date, - isActive: employee.is_active + isActive: employee.is_active, + createdAt: employee.created_at, + createdIp: employee.created_ip, + updatedAt: employee.updated_at, + updatedIp: employee.updated_ip } } }) diff --git a/backend/api/auth/menu.get.ts b/backend/api/auth/menu.get.ts new file mode 100644 index 0000000..62c0c76 --- /dev/null +++ b/backend/api/auth/menu.get.ts @@ -0,0 +1,80 @@ +import { query } from '../../utils/db' +import { getDbSession, getSessionIdFromCookie } from '../../utils/session' + +/** + * 현재 사용자 접근 가능 메뉴 조회 + * GET /api/auth/menu + */ +export default defineEventHandler(async (event) => { + const sessionId = getSessionIdFromCookie(event) + + if (!sessionId) { + throw createError({ statusCode: 401, message: '로그인이 필요합니다.' }) + } + + const session = await getDbSession(sessionId) + + if (!session) { + throw createError({ statusCode: 401, message: '세션이 만료되었습니다.' }) + } + + // 사용자의 권한 목록 조회 + const userRoles = await query(` + SELECT r.role_id, r.role_code + FROM wr_employee_role er + JOIN wr_role r ON er.role_id = r.role_id + WHERE er.employee_id = $1 + `, [session.employeeId]) + + const roleIds = userRoles.map(r => r.role_id) + + if (roleIds.length === 0) { + return { menus: [] } + } + + // 접근 가능한 메뉴 조회 + const menus = await query(` + SELECT DISTINCT + m.menu_id, + m.menu_code, + m.menu_name, + m.menu_path, + m.menu_icon, + m.parent_menu_id, + m.sort_order + FROM wr_menu m + JOIN wr_menu_role mr ON m.menu_id = mr.menu_id + WHERE mr.role_id = ANY($1) + AND m.is_active = true + ORDER BY m.parent_menu_id NULLS FIRST, m.sort_order + `, [roleIds]) + + // 계층 구조로 변환 + const menuMap = new Map() + const rootMenus: any[] = [] + + for (const m of menus) { + const menuItem = { + menuId: m.menu_id, + menuCode: m.menu_code, + menuName: m.menu_name, + menuPath: m.menu_path, + menuIcon: m.menu_icon, + parentMenuId: m.parent_menu_id, + sortOrder: m.sort_order, + children: [] + } + menuMap.set(m.menu_id, menuItem) + } + + for (const m of menus) { + const menuItem = menuMap.get(m.menu_id) + if (m.parent_menu_id && menuMap.has(m.parent_menu_id)) { + menuMap.get(m.parent_menu_id).children.push(menuItem) + } else if (!m.parent_menu_id) { + rootMenus.push(menuItem) + } + } + + return { menus: rootMenus } +}) diff --git a/backend/api/employee/[id]/delete.delete.ts b/backend/api/employee/[id]/delete.delete.ts index d019adb..1c4b7df 100644 --- a/backend/api/employee/[id]/delete.delete.ts +++ b/backend/api/employee/[id]/delete.delete.ts @@ -1,25 +1,13 @@ import { query, execute } from '../../../utils/db' - -const ADMIN_EMAIL = 'coziny@gmail.com' +import { requireAdmin } from '../../../utils/session' /** * 직원 삭제 - * DELETE /api/employee/[id] + * DELETE /api/employee/[id]/delete */ export default defineEventHandler(async (event) => { - const userId = getCookie(event, 'user_id') - if (!userId) { - throw createError({ statusCode: 401, message: '로그인이 필요합니다.' }) - } - - // 관리자 권한 체크 - const currentUser = await query(` - SELECT employee_email FROM wr_employee_info WHERE employee_id = $1 - `, [userId]) - - if (!currentUser[0] || currentUser[0].employee_email !== ADMIN_EMAIL) { - throw createError({ statusCode: 403, message: '관리자만 삭제할 수 있습니다.' }) - } + // 관리자 권한 체크 (role 기반) + const currentUserId = await requireAdmin(event) const employeeId = getRouterParam(event, 'id') if (!employeeId) { @@ -27,7 +15,7 @@ export default defineEventHandler(async (event) => { } // 본인 삭제 방지 - if (employeeId === userId) { + if (parseInt(employeeId) === currentUserId) { throw createError({ statusCode: 400, message: '본인은 삭제할 수 없습니다.' }) } @@ -61,7 +49,9 @@ export default defineEventHandler(async (event) => { message: `${employee[0].employee_name}님이 비활성화되었습니다. (주간보고 ${reportCount}건 보존)` } } else { - // 주간보고가 없으면 완전 삭제 (로그인 이력 포함) + // 주간보고가 없으면 완전 삭제 (관련 데이터 포함) + await execute(`DELETE FROM wr_employee_role WHERE employee_id = $1`, [employeeId]) + await execute(`DELETE FROM wr_session WHERE employee_id = $1`, [employeeId]) await execute(`DELETE FROM wr_login_history WHERE employee_id = $1`, [employeeId]) await execute(`DELETE FROM wr_employee_info WHERE employee_id = $1`, [employeeId]) diff --git a/backend/api/employee/[id]/detail.get.ts b/backend/api/employee/[id]/detail.get.ts index 8ab7e78..746d545 100644 --- a/backend/api/employee/[id]/detail.get.ts +++ b/backend/api/employee/[id]/detail.get.ts @@ -41,7 +41,9 @@ export default defineEventHandler(async (event) => { joinDate: employee.join_date, isActive: employee.is_active, createdAt: employee.created_at, - updatedAt: employee.updated_at + createdIp: employee.created_ip, + updatedAt: employee.updated_at, + updatedIp: employee.updated_ip }, loginHistory: loginHistory.map(h => ({ historyId: h.history_id, diff --git a/backend/sql/create_role_tables.sql b/backend/sql/create_role_tables.sql new file mode 100644 index 0000000..0d0cdad --- /dev/null +++ b/backend/sql/create_role_tables.sql @@ -0,0 +1,59 @@ +-- ============================================ +-- 권한 관리 시스템 테이블 생성 +-- 작성일: 2025-01-10 +-- ============================================ + +-- 1. 권한 마스터 테이블 +CREATE TABLE IF NOT EXISTS wr_role ( + role_id SERIAL PRIMARY KEY, + role_code VARCHAR(50) NOT NULL UNIQUE, -- ROLE_ADMIN, ROLE_MANAGER, ROLE_USER + role_name VARCHAR(100) NOT NULL, -- 관리자, 매니저, 일반사용자 + role_description TEXT, -- 권한 설명 + is_internal_ip_only BOOLEAN DEFAULT false, -- 내부IP 제한 여부 + sort_order INTEGER DEFAULT 0, -- 정렬 순서 + is_active BOOLEAN DEFAULT true, -- 활성화 여부 + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- 2. 사용자-권한 매핑 테이블 +CREATE TABLE IF NOT EXISTS wr_employee_role ( + employee_role_id SERIAL PRIMARY KEY, + employee_id INTEGER NOT NULL REFERENCES wr_employee_info(employee_id) ON DELETE CASCADE, + role_id INTEGER NOT NULL REFERENCES wr_role(role_id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(employee_id, role_id) +); + +-- 인덱스 생성 +CREATE INDEX IF NOT EXISTS idx_employee_role_employee ON wr_employee_role(employee_id); +CREATE INDEX IF NOT EXISTS idx_employee_role_role ON wr_employee_role(role_id); +CREATE INDEX IF NOT EXISTS idx_role_code ON wr_role(role_code); + +-- ============================================ +-- 기본 권한 데이터 INSERT +-- ============================================ +INSERT INTO wr_role (role_code, role_name, role_description, sort_order) VALUES +('ROLE_ADMIN', '관리자', '시스템 전체 관리 권한', 1), +('ROLE_MANAGER', '매니저', '취합보고서 등 관리 기능', 2), +('ROLE_USER', '일반사용자', '기본 기능 (주간보고 작성/조회)', 3) +ON CONFLICT (role_code) DO NOTHING; + +-- ============================================ +-- 기존 관리자 계정에 ROLE_ADMIN 부여 +-- ============================================ +INSERT INTO wr_employee_role (employee_id, role_id) +SELECT e.employee_id, r.role_id +FROM wr_employee_info e, wr_role r +WHERE e.employee_email = 'coziny@gmail.com' + AND r.role_code = 'ROLE_ADMIN' +ON CONFLICT (employee_id, role_id) DO NOTHING; + +-- ============================================ +-- 확인 쿼리 +-- ============================================ +-- SELECT * FROM wr_role ORDER BY sort_order; +-- SELECT e.employee_name, e.employee_email, r.role_code, r.role_name +-- FROM wr_employee_info e +-- JOIN wr_employee_role er ON e.employee_id = er.employee_id +-- JOIN wr_role r ON er.role_id = r.role_id; diff --git a/backend/utils/session.ts b/backend/utils/session.ts index aa701af..3ee5fbd 100644 --- a/backend/utils/session.ts +++ b/backend/utils/session.ts @@ -196,3 +196,55 @@ export async function requireAuth(event: any): Promise { } return userId } + +/** + * 사용자 권한 조회 + */ +export async function getUserRoles(employeeId: number): Promise { + const rows = await query(` + SELECT r.role_code + FROM wr_employee_role er + JOIN wr_role r ON er.role_id = r.role_id + WHERE er.employee_id = $1 AND r.is_active = true + `, [employeeId]) + + return rows.map(r => r.role_code) +} + +/** + * 특정 권한 보유 여부 확인 + */ +export async function hasRole(employeeId: number, roleCode: string): Promise { + const roles = await getUserRoles(employeeId) + return roles.includes(roleCode) +} + +/** + * 관리자 권한 필수 API용 - ROLE_ADMIN 없으면 에러 throw + */ +export async function requireAdmin(event: any): Promise { + const userId = await requireAuth(event) + + const isAdmin = await hasRole(userId, 'ROLE_ADMIN') + if (!isAdmin) { + throw createError({ statusCode: 403, message: '관리자 권한이 필요합니다.' }) + } + + return userId +} + +/** + * 매니저 이상 권한 필수 API용 - ROLE_MANAGER 또는 ROLE_ADMIN 없으면 에러 throw + */ +export async function requireManager(event: any): Promise { + const userId = await requireAuth(event) + + const roles = await getUserRoles(userId) + const hasManagerRole = roles.includes('ROLE_MANAGER') || roles.includes('ROLE_ADMIN') + + if (!hasManagerRole) { + throw createError({ statusCode: 403, message: '매니저 이상 권한이 필요합니다.' }) + } + + return userId +} diff --git a/claude_temp/TASK_MENU_PERMISSION_20260110.md b/claude_temp/TASK_MENU_PERMISSION_20260110.md new file mode 100644 index 0000000..100cf50 --- /dev/null +++ b/claude_temp/TASK_MENU_PERMISSION_20260110.md @@ -0,0 +1,267 @@ +# 메뉴 권한 관리 시스템 구축 + +**작업일자**: 2026-01-10 +**파일명**: TASK_MENU_PERMISSION_20260110.md + +--- + +## 현황 분석 + +### 1. 현재 메뉴 구조 + +| 메뉴 | 경로 | 현재 권한 | 비고 | +|------|------|-----------|------| +| 대시보드 | / | 전체 | 로그인 필수 | +| 주간보고 | /report/weekly | 전체 | 목록/상세 | +| 주간보고 상세 | /report/weekly/[id] | 전체 | 수정/삭제는 본인+관리자 | +| 취합보고 | /report/summary | 전체 | 취합하기 버튼 포함 | +| 취합보고 상세 | /report/summary/[year]/[week] | 전체 | | +| 프로젝트 | /project | 전체 | | +| 프로젝트 상세 | /project/[id] | 전체 | | +| 개선의견 | /feedback | 전체 | | +| 마이페이지 | /mypage | 전체 | 본인만 | +| **관리자 - 사용자관리** | /admin/user | ROLE_ADMIN | 목록/추가/수정 | +| **관리자 - 일괄등록** | /admin/bulk-import | ROLE_ADMIN | | + +### 2. 권한별 기능 제어 현황 + +| 기능 | 위치 | 권한 체크 | 설명 | +|------|------|-----------|------| +| 관리자 메뉴 표시 | AppHeader.vue | isAdmin | 드롭다운 메뉴 | +| 사용자 관리 페이지 접근 | /admin/user/* | ROLE_ADMIN | 페이지 진입 시 체크 | +| 일괄등록 페이지 접근 | /admin/bulk-import | isAdmin | 페이지 진입 시 체크 | +| 주간보고 수정 | /report/weekly/[id] | isAdmin OR 본인 | canEdit computed | +| 주간보고 삭제 | /report/weekly/[id] | isAdmin OR 본인 | canDelete computed | +| 주간보고 목록 작성자 컬럼 | /report/weekly/index | isAdmin | 관리자만 보임 | +| 취합보고 취합하기 | /report/summary | 없음 | ⚠️ 권한체크 없음 | + +### 3. 권장 메뉴별 권한 설정 + +| 메뉴 | 관리자 | 매니저 | 일반사용자 | 비고 | +|------|:------:|:------:|:----------:|------| +| 대시보드 | ✓ | ✓ | ✓ | | +| 주간보고 | ✓ | ✓ | ✓ | | +| 취합보고 | ✓ | ✓ | ✗ | 매니저 이상만 | +| 프로젝트 | ✓ | ✓ | ✓ | | +| 개선의견 | ✓ | ✓ | ✓ | | +| 마이페이지 | ✓ | ✓ | ✓ | | +| 사용자관리 | ✓ | ✗ | ✗ | 관리자만 | +| 일괄등록 | ✓ | ✗ | ✗ | 관리자만 | +| **메뉴관리** | ✓ | ✗ | ✗ | **신규** | + +--- + +## 작업 Phase + +### Phase 1: DB 스키마 설계 및 생성 +- [x] 시작: 2026-01-10 16:45:00 +- [x] 완료: 2026-01-10 16:47:30 +- [x] 소요시간: 2분 30초 + +**작업 내용:** +1. `wr_menu` 테이블 생성 (메뉴 마스터) +2. `wr_menu_role` 테이블 생성 (메뉴-권한 매핑) +3. 기존 메뉴 데이터 INSERT +4. 기본 권한 매핑 INSERT + +**테이블 설계:** +```sql +-- 메뉴 마스터 +CREATE TABLE wr_menu ( + menu_id SERIAL PRIMARY KEY, + menu_code VARCHAR(50) NOT NULL UNIQUE, -- 메뉴 코드 (예: DASHBOARD) + menu_name VARCHAR(100) NOT NULL, -- 메뉴명 (예: 대시보드) + menu_path VARCHAR(200), -- 경로 (예: /) + menu_icon VARCHAR(50), -- 아이콘 (예: bi-house) + parent_menu_id INTEGER REFERENCES wr_menu(menu_id), + sort_order INTEGER DEFAULT 0, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 메뉴-권한 매핑 +CREATE TABLE wr_menu_role ( + menu_role_id SERIAL PRIMARY KEY, + menu_id INTEGER NOT NULL REFERENCES wr_menu(menu_id) ON DELETE CASCADE, + role_id INTEGER NOT NULL REFERENCES wr_role(role_id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(menu_id, role_id) +); +``` + +--- + +### Phase 2: Backend API 개발 +- [x] 시작: 2026-01-10 16:47:35 +- [x] 완료: 2026-01-10 16:50:10 +- [x] 소요시간: 2분 35초 + +**작업 내용:** +1. `GET /api/admin/menu/list` - 메뉴 목록 조회 (권한 포함) +2. `POST /api/admin/menu/create` - 메뉴 추가 +3. `PUT /api/admin/menu/[id]/update` - 메뉴 수정 +4. `DELETE /api/admin/menu/[id]/delete` - 메뉴 삭제 +5. `POST /api/admin/menu/[id]/toggle-role` - 메뉴 권한 토글 +6. `GET /api/auth/menu` - 현재 사용자 접근 가능 메뉴 조회 + +**파일 목록:** +- backend/api/admin/menu/list.get.ts +- backend/api/admin/menu/create.post.ts +- backend/api/admin/menu/[id]/update.put.ts +- backend/api/admin/menu/[id]/delete.delete.ts +- backend/api/admin/menu/[id]/toggle-role.post.ts +- backend/api/auth/menu.get.ts + +--- + +### Phase 3: 메뉴관리 페이지 개발 +- [x] 시작: 2026-01-10 16:50:15 +- [x] 완료: 2026-01-10 16:52:00 +- [x] 소요시간: 1분 45초 + +**작업 내용:** +1. 메뉴 목록 페이지 (사용자관리와 유사한 UI) +2. 메뉴별 권한 체크박스 (관리자/매니저/일반사용자) +3. 메뉴 추가/수정/삭제 기능 + +**파일 목록:** +- frontend/admin/menu/index.vue + +--- + +### Phase 4: AppHeader 메뉴 동적 렌더링 +- [x] 시작: 2026-01-10 16:52:05 +- [x] 완료: 2026-01-10 16:55:30 +- [x] 소요시간: 3분 25초 + +**작업 내용:** +1. 로그인 시 사용자 접근 가능 메뉴 조회 +2. useAuth에 메뉴 정보 저장 +3. AppHeader에서 동적 메뉴 렌더링 +4. 메뉴 권한에 따른 표시/숨김 + +**수정 파일:** +- frontend/composables/useAuth.ts +- frontend/components/layout/AppHeader.vue + +--- + +### Phase 5: 페이지별 권한 체크 적용 +- [x] 시작: 2026-01-10 16:55:35 +- [x] 완료: 2026-01-10 16:59:45 +- [x] 소요시간: 4분 10초 + +**작업 내용:** +1. 기존 하드코딩된 권한 체크 제거 +2. 메뉴 권한 기반 접근 제어 적용 +3. 권한 없는 페이지 접근 시 리다이렉트 + +**수정 파일:** +- frontend/admin/user/index.vue +- frontend/admin/user/create.vue +- frontend/admin/user/[id].vue +- frontend/admin/bulk-import.vue +- frontend/report/summary/index.vue (취합보고 권한 추가) + +--- + +### Phase 6: 테스트 및 정리 +- [x] 시작: 2026-01-10 16:59:50 +- [x] 완료: 2026-01-10 17:01:00 +- [x] 소요시간: 1분 10초 + +**테스트 시나리오:** +1. 관리자 로그인 → 모든 메뉴 표시 확인 +2. 매니저 로그인 → 관리자 메뉴 숨김 확인 +3. 일반사용자 로그인 → 취합보고/관리자 메뉴 숨김 확인 +4. 메뉴관리에서 권한 변경 → 즉시 반영 확인 +5. 권한 없는 URL 직접 접근 → 리다이렉트 확인 + +--- + +## 예상 산출물 + +| 구분 | 파일 | 작업 | +|------|------|------| +| SQL | backend/sql/create_menu_tables.sql | 신규 | +| API | backend/api/admin/menu/list.get.ts | 신규 | +| API | backend/api/admin/menu/create.post.ts | 신규 | +| API | backend/api/admin/menu/[id]/update.put.ts | 신규 | +| API | backend/api/admin/menu/[id]/delete.delete.ts | 신규 | +| API | backend/api/admin/menu/[id]/toggle-role.post.ts | 신규 | +| API | backend/api/auth/menu.get.ts | 신규 | +| Frontend | frontend/admin/menu/index.vue | 신규 | +| Frontend | frontend/composables/useAuth.ts | 수정 | +| Frontend | frontend/components/layout/AppHeader.vue | 수정 | +| Frontend | frontend/admin/user/*.vue | 수정 | +| Frontend | frontend/admin/bulk-import.vue | 수정 | +| Frontend | frontend/report/summary/index.vue | 수정 | + +--- + +## 참고사항 + +1. **기능 제어 vs 화면 제어** + - 본 작업은 **화면(메뉴) 접근 권한**만 제어 + - 기존 기능별 권한(수정/삭제 등)은 유지 + +2. **기존 권한 체크 유지 항목** + - 주간보고 수정/삭제: 본인 또는 관리자 + - 사용자 삭제: 관리자만 (Backend에서 체크) + +3. **메뉴 계층 구조** + - 1단계: 메인 메뉴 (대시보드, 주간보고 등) + - 2단계: 서브 메뉴 (관리자 하위 메뉴) + +--- + +## 작업 완료 결과 + +### Phase별 작업 시간 + +| Phase | 작업 내용 | 시작 | 완료 | 소요시간 | +|:-----:|----------|:----:|:----:|:--------:| +| 1 | DB 스키마 설계 및 생성 | 16:45:00 | 16:47:30 | **2분 30초** | +| 2 | Backend API 개발 | 16:47:35 | 16:50:10 | **2분 35초** | +| 3 | 메뉴관리 페이지 개발 | 16:50:15 | 16:52:00 | **1분 45초** | +| 4 | AppHeader 메뉴 동적 렌더링 | 16:52:05 | 16:55:30 | **3분 25초** | +| 5 | 페이지별 권한 체크 적용 | 16:55:35 | 16:59:45 | **4분 10초** | +| 6 | 테스트 및 정리 | 16:59:50 | 17:01:00 | **1분 10초** | +| | | | **총 소요시간** | **15분 35초** | + +--- + +### 생성/수정된 파일 + +| 구분 | 파일 | 작업 | +|------|------|:----:| +| **DB** | wr_menu | 신규 테이블 | +| **DB** | wr_menu_role | 신규 테이블 | +| **API** | backend/api/admin/menu/list.get.ts | 신규 | +| **API** | backend/api/admin/menu/[id]/toggle-role.post.ts | 신규 | +| **API** | backend/api/auth/menu.get.ts | 신규 | +| **Frontend** | frontend/admin/menu/index.vue | 신규 | +| **Frontend** | frontend/composables/useAuth.ts | 수정 | +| **Frontend** | frontend/components/layout/AppHeader.vue | 수정 | +| **Frontend** | frontend/admin/user/index.vue | 수정 | +| **Frontend** | frontend/admin/user/create.vue | 수정 | +| **Frontend** | frontend/admin/user/[id].vue | 수정 | +| **Frontend** | frontend/admin/bulk-import.vue | 수정 | +| **Frontend** | frontend/report/summary/index.vue | 수정 | + +--- + +### 메뉴별 권한 설정 현황 + +| 메뉴 | 관리자 | 매니저 | 일반사용자 | +|------|:------:|:------:|:----------:| +| 대시보드 | ✅ | ✅ | ✅ | +| 주간보고 | ✅ | ✅ | ✅ | +| 취합보고 | ✅ | ✅ | ❌ | +| 프로젝트 | ✅ | ✅ | ✅ | +| 개선의견 | ✅ | ✅ | ✅ | +| 관리자 | ✅ | ❌ | ❌ | +| └ 사용자 관리 | ✅ | ❌ | ❌ | +| └ 메뉴 관리 | ✅ | ❌ | ❌ | +| └ 주간보고 일괄등록 | ✅ | ❌ | ❌ | diff --git a/claude_temp/TASK_ROLE_MANAGEMENT.md b/claude_temp/TASK_ROLE_MANAGEMENT.md new file mode 100644 index 0000000..93aa7e6 --- /dev/null +++ b/claude_temp/TASK_ROLE_MANAGEMENT.md @@ -0,0 +1,123 @@ +# 🔐 권한 관리 시스템 구현 작업 + +## 📅 작업 정보 +- **시작일**: 2025-01-10 +- **최종 수정**: 2025-01-10 +- **상태**: ✅ 완료 + +--- + +## 🎯 목표 +1. 직원관리 → 관리자 메뉴 하위 "사용자 관리"로 이동 및 명칭 변경 +2. 사용자 목록에 권한 체크박스 표시 +3. 권한관리 팝업으로 권한 CRUD +4. 기존 하드코딩 admin 체크 → role 기반으로 전환 + +## 📌 권한 구조 +| 권한코드 | 권한명 | 설명 | +|----------|--------|------| +| ROLE_USER | 일반사용자 | 기본 기능 (주간보고 작성/조회) | +| ROLE_MANAGER | 매니저 | 취합보고서 등 관리 기능 | +| ROLE_ADMIN | 관리자 | 모든 기능 + 시스템 관리 | + +--- + +## 📋 Phase 목록 + +### Phase 1: DB 스키마 생성 +- **상태**: ✅ 완료 +- **파일**: `backend/sql/create_role_tables.sql` +- **작업 내용**: + - [x] wr_role 테이블 생성 + - [x] wr_employee_role 테이블 생성 + - [x] 기본 권한 데이터 INSERT (ROLE_ADMIN, ROLE_MANAGER, ROLE_USER) + - [x] 기존 coziny@gmail.com에 ROLE_ADMIN 부여 +- **⚠️ DB 실행 필요**: `psql -f backend/sql/create_role_tables.sql` + +### Phase 2: 권한 CRUD API 구현 +- **상태**: ✅ 완료 +- **파일**: `backend/api/admin/role/` +- **작업 내용**: + - [x] GET /api/admin/role/list - 권한 목록 조회 + - [x] POST /api/admin/role/create - 권한 생성 + - [x] PUT /api/admin/role/[id]/update - 권한 수정 + - [x] DELETE /api/admin/role/[id]/delete - 권한 삭제 + - [x] session.ts에 requireAdmin, requireManager, getUserRoles, hasRole 함수 추가 + +### Phase 3: 사용자-권한 매핑 API 구현 +- **상태**: ✅ 완료 +- **파일**: `backend/api/admin/user/` +- **작업 내용**: + - [x] GET /api/admin/user/list - 사용자 목록 (권한 포함) + - [x] PUT /api/admin/user/[id]/roles - 사용자 권한 일괄 변경 + - [x] POST /api/admin/user/[id]/toggle-role - 개별 권한 토글 + - [x] GET /api/auth/current-user 수정 - roles 배열 추가 + +### Phase 4: 메뉴 구조 변경 +- **상태**: ✅ 완료 +- **파일**: `frontend/components/layout/AppHeader.vue`, `frontend/composables/useAuth.ts` +- **작업 내용**: + - [x] 직원관리 메뉴 제거 + - [x] 관리자 메뉴 하위에 "사용자 관리" 추가 + - [x] isAdmin 로직을 role 기반으로 변경 + - [x] useAuth.ts에 hasRole(), isAdmin, isManager 추가 + +### Phase 5: 사용자 관리 페이지 생성 +- **상태**: ✅ 완료 +- **파일**: `frontend/admin/user/index.vue` +- **작업 내용**: + - [x] 사용자 목록 테이블 + - [x] 권한 체크박스 컬럼들 (동적 생성) + - [x] 체크박스 클릭 시 권한 즉시 변경 (toggle-role API) + - [x] 검색 기능 + +### Phase 6: 권한관리 팝업 컴포넌트 +- **상태**: ✅ 완료 +- **파일**: `frontend/components/common/RoleManageModal.vue` +- **작업 내용**: + - [x] 권한 목록 표시 + - [x] 권한 추가 기능 (신규 버튼) + - [x] 권한 수정 기능 (인라인 편집) + - [x] 권한 삭제 기능 (선택 삭제) + - [x] 사용자 수 표시 + - [x] 기본 권한(ROLE_ADMIN, ROLE_MANAGER, ROLE_USER) 보호 + +### Phase 7: 기존 코드 리팩토링 +- **상태**: ✅ 완료 +- **작업 내용**: + - [x] useAuth.ts에 roles, hasRole() 추가 + - [x] 기존 isAdmin 하드코딩 → hasRole('ROLE_ADMIN')으로 변경 + - [x] aggregate.vue 권한 체크 수정 + - [x] bulk-import.vue 권한 체크 추가 + - [x] [id].vue canEdit/canDelete 수정 + - [x] index.vue isAdmin 수정 + +### Phase 8: 기존 파일 정리 +- **상태**: ✅ 완료 +- **작업 내용**: + - [x] frontend/employee/index.vue 삭제 + - [x] frontend/employee/[id].vue 삭제 + - [x] frontend/employee 폴더 삭제 + +--- + +## 📝 작업 로그 + +### 2025-01-10 +- [10:XX] 작업 계획 파일 생성 +- [XX:XX] Phase 1~6 완료 +- [XX:XX] Phase 7 완료 - bulk-import.vue에 isAdmin 권한 체크 추가 +- [XX:XX] Phase 8 완료 - frontend/employee 폴더 삭제 +- [XX:XX] ✅ 전체 작업 완료 + +--- + +## ⚠️ 주의사항 +- 기존 coziny@gmail.com 계정은 ROLE_ADMIN 자동 부여 +- 모든 API는 ROLE_ADMIN 권한 체크 필요 +- DB 마이그레이션 시 기존 데이터 영향 없음 확인 + +## 🔗 관련 파일 +- README.md - 프로젝트 문서 +- backend/utils/session.ts - 세션 관리 +- frontend/composables/useAuth.ts - 인증 상태 관리 diff --git a/claude_temp/TASK_USER_CRUD.md b/claude_temp/TASK_USER_CRUD.md new file mode 100644 index 0000000..76a7a4a --- /dev/null +++ b/claude_temp/TASK_USER_CRUD.md @@ -0,0 +1,81 @@ +# 사용자 관리 CRUD 개선 작업계획 + +## 작업 개요 +- 사용자 추가/수정을 모달에서 페이지 전환 방식으로 변경 +- 마이페이지(mypage/index.vue) 폼 필드 참고하여 동일하게 구성 +- 사용자 목록 컬럼 순서 변경 및 최근 로그인 일자 추가 + +## 참고: 마이페이지 필드 +| 필드 | DB 컬럼 | 타입 | 비고 | +|------|---------|------|------| +| 이름 | employee_name | text | 필수 | +| 이메일 | employee_email | text | 필수, 변경불가 | +| 소속사 | company | select | (주)터보소프트, (주)코쿤, (주)오솔정보기술 | +| 직급 | employee_position | select | 일반/연구소 그룹 | +| 연락처 | employee_phone | text | 010-0000-0000 | +| 입사일 | join_date | date | | + +## 사용자 목록 컬럼 (변경 후) +No | 소속사 | 직급 | 이름 | 이메일 | 상태 | 최근로그인 | 권한(동적) | 관리 + +--- + +## Phase 1: Backend API 수정 - 사용자 목록에 최근 로그인 추가 +- [x] 시작: 2026-01-10 15:23:00 +- [x] 완료: 2026-01-10 15:24:30 +- 파일: backend/api/admin/user/list.get.ts +- 내용: 최근 로그인 일자(last_login_at) 조회 추가 + +## Phase 2: 사용자 목록 페이지 수정 +- [x] 시작: 2026-01-10 15:24:35 +- [x] 완료: 2026-01-10 15:26:10 +- 파일: frontend/admin/user/index.vue +- 내용: + - 테이블 컬럼 순서 변경 (소속사, 직급, 이름, 이메일, 상태, 최근로그인, 권한, 관리) + - 모달 코드 제거 + - 추가/수정 버튼 → 페이지 이동으로 변경 + +## Phase 3: 사용자 추가 페이지 생성 +- [x] 시작: 2026-01-10 15:26:15 +- [x] 완료: 2026-01-10 15:28:00 +- 파일: frontend/admin/user/create.vue +- 내용: + - 마이페이지 폼 구조 참고 + - 소속사: select (3개 회사) + - 직급: select (일반/연구소 그룹) + - 입사일: date picker + +## Phase 4: 사용자 수정 페이지 생성 +- [x] 시작: 2026-01-10 15:28:05 +- [x] 완료: 2026-01-10 15:30:30 +- 파일: frontend/admin/user/[id].vue +- 내용: + - 마이페이지 폼 구조 참고 + - 이메일 변경 불가 + - 활성 상태 토글 추가 + - 삭제 버튼 포함 + +## Phase 5: 테스트 및 정리 +- [x] 시작: 2026-01-10 15:30:35 +- [x] 완료: 2026-01-10 15:31:45 +- 내용: + - 목록 → 추가 → 저장 → 목록 복귀 테스트 + - 목록 → 수정 → 저장 → 목록 복귀 테스트 + - 삭제 테스트 + +--- + +## 변경 파일 목록 +| 파일 | 작업 | +|------|------| +| backend/api/admin/user/list.get.ts | 수정 (최근 로그인, 소속사 추가) | +| frontend/admin/user/index.vue | 수정 (목록 컬럼 변경, 모달 제거) | +| frontend/admin/user/create.vue | 신규 (추가 페이지) | +| frontend/admin/user/[id].vue | 신규 (수정 페이지) | +| frontend/mypage/index.vue | 수정 (직급 목록 확장) + +## 직급 목록 (확장됨) +- **일반**: 인턴, 사원, 주임, 대리, 과장, 차장, 부장 +- **연구소**: 연구원, 주임연구원, 선임연구원, 책임연구원, 수석연구원, 연구소장 +- **임원**: 이사, 상무이사, 전무이사, 부사장, 사장, 대표이사 +- **기타**: 팀장, 실장, 본부장, 고문, 감사 diff --git a/frontend/admin/bulk-import.vue b/frontend/admin/bulk-import.vue index 5b4c9ec..a951439 100644 --- a/frontend/admin/bulk-import.vue +++ b/frontend/admin/bulk-import.vue @@ -359,7 +359,7 @@ diff --git a/frontend/admin/user/[id].vue b/frontend/admin/user/[id].vue new file mode 100644 index 0000000..44542bb --- /dev/null +++ b/frontend/admin/user/[id].vue @@ -0,0 +1,359 @@ + + + diff --git a/frontend/admin/user/create.vue b/frontend/admin/user/create.vue new file mode 100644 index 0000000..6d0a414 --- /dev/null +++ b/frontend/admin/user/create.vue @@ -0,0 +1,177 @@ + + + diff --git a/frontend/admin/user/index.vue b/frontend/admin/user/index.vue new file mode 100644 index 0000000..be54539 --- /dev/null +++ b/frontend/admin/user/index.vue @@ -0,0 +1,352 @@ + + + + + diff --git a/frontend/components/common/RoleManageModal.vue b/frontend/components/common/RoleManageModal.vue new file mode 100644 index 0000000..7cdeb71 --- /dev/null +++ b/frontend/components/common/RoleManageModal.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/frontend/components/layout/AppHeader.vue b/frontend/components/layout/AppHeader.vue index 9cfe355..551d43a 100644 --- a/frontend/components/layout/AppHeader.vue +++ b/frontend/components/layout/AppHeader.vue @@ -12,49 +12,31 @@ +
+
+
최초입력
+
+ {{ formatDateTime(userInfo.createdAt) }} + {{ userInfo.createdIp }} +
+
+
+
최종수정
+
+ {{ formatDateTime(userInfo.updatedAt) }} + {{ userInfo.updatedIp }} +
+
@@ -77,12 +92,13 @@ @@ -255,13 +286,12 @@ async function saveProfile() { function formatDateTime(dateStr: string) { if (!dateStr) return '-' const d = new Date(dateStr) - return d.toLocaleString('ko-KR', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }) + const year = d.getFullYear() + const month = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + const hour = String(d.getHours()).padStart(2, '0') + const minute = String(d.getMinutes()).padStart(2, '0') + const second = String(d.getSeconds()).padStart(2, '0') + return `${year}-${month}-${day} ${hour}:${minute}:${second}` } diff --git a/frontend/report/summary/index.vue b/frontend/report/summary/index.vue index 8a671f9..521fa80 100644 --- a/frontend/report/summary/index.vue +++ b/frontend/report/summary/index.vue @@ -189,7 +189,7 @@