1. 사용자 관리 모듈의 역할
관리 시스(MIS)에서 사용자 관리는 권한 기반 접근 제어(RBAC)의 출발점이다. 이 모듈은 시스템 로그인 계정의 등록, 수정, 삭제, 비밀번호 재설정, 활성/비활성 처리 등을 담당하며, 이후 역할(Role) 배정, 메뉴 한, 업무 권한 등 모든 권한 설정의 기준 데이터가 된다.
보안상 다음 사항을 수한다.
- 최고 관리자(Administrator) 계정은 수정·삭제할 수 없다.
- 현재 로그인한 사용자 본인 계정은 삭제할 수 없다.
- 삭제는 물리 삭제 대신 제 플래그(DelFlag)를 사용하는 논리 삭제로 처리한다.
2. 마크업과 한 기반 툴바
마스터 페이지 기반의 ASP.NET WebForms 페이지에서 화면을 구성한다. 툴 영역은 서버 측 권한 정보에 따라 동적으로 생성되며, 권한이 없는 버튼은 disabled 상태로 렌더링된다.
<%@ Page Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="UserMgr.aspx.cs" Inherits="App.Modules.UserMgr" %>
<asp:Content ID="HeadContent" ContentPlaceHolderID="head" runat="server">
<script src="../Scripts/jquery.easyui.min.js"></script>
<script src="../Scripts/Business/UserMgr.js"></script>
</asp:Content>
<asp:Content ID="MainContent" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
<div class="toolbar"><%= BuildActionButtons() %></div>
<table id="dgUser" class="scroll"></table>
<div id="winUser"></div>
<div id="winPwd"></div>
</asp:Content>
public partial class UserMgr : BasePage
{
protected bool allowAdd, allowEdit, allowDelete, allowResetPwd;
protected void Page_Load(object sender, EventArgs e)
{
GetPermission();
}
private void GetPermission()
{
allowAdd = IsAuthorized("User.Add");
allowEdit = IsAuthorized("User.Edit");
allowDelete = IsAuthorized("User.Delete");
allowResetPwd = IsAuthorized("User.ResetPassword");
}
protected string BuildActionButtons()
{
var html = new StringBuilder();
string tpl = "<a id=\"btn{0}\" class=\"easyui-linkbutton\" style=\"float:left\" plain=\"true\" iconCls=\"{1}\" {2} title=\"{3}\">{4}</a>";
html.Append(string.Format(tpl, "Refresh", "icon-reload", "", "새로고침", "로고침"));
html.Append("<div class='datagrid-btn-separator'></div>");
html.Append(string.Format(tpl, "Add", "icon-user_add", allowAdd ? "" : "disabled=\"true\"", "사용자 추가", "추가"));
html.Append(string.Format(tpl, "Edit", "icon-user_edit", allowEdit ? "" : "disabled=\"true\"", "사용자 수정", "수정"));
html.Append(string.Format(tpl, "Delete", "icon-user_delete", allowDelete ? "" : "disabled=\"true\"", "사용자 삭제", "제"));
html.Append("<div class='datagrid-btn-separator'></div>");
html.Append(string.Format(tpl, "ResetPwd","icon-user_key", allowResetPwd ? "" : "disabled=\"true\"", "비밀번호 재설정", "비밀번호"));
return html.ToString();
}
}
3. DataGrid 데이터 바인딩
EasyUI DataGrid는 서버 핸들러에서 JSON 목록을 받아 렌더링한다. 다음 예제는 그리드 초기화 로직을 userGrid 객체로 분리하고, 활성 상태는 이미지로 변환하는 formatter를 사용한다.
const currentUser = {
id: parseInt("<%= UserInfo.Id %>", 10),
userName: "<%= UserInfo.UserName %>",
realName: "<%= UserInfo.RealName %>"
};
const userGrid = {
init() {
const w = $(window).width() - 4;
const h = $(window).height() - 40;
$("#dgUser").datagrid({
url: "handler/UserHandler.ashx",
title: "시스템 사용자 목록",
iconCls: "icon-list",
width: w,
height: h,
nowrap: true,
rownumbers: true,
striped: true,
idField: "Id",
singleSelect: true,
checkOnSelect: true,
columns: [[
{ field: "Id", hidden: true },
{ title: "아이디", field: "UserName", width: 120, sortable: true },
{ title: "이름", field: "RealName", width: 120 },
{ title: "부서", field: "DepartmentName", width: 160 },
{ title: "활성", field: "Enabled", width: 60, align: "center", formatter: formatEnabled },
{ title: "이메일", field: "Email", width: 180 },
{ title: "전화", field: "Mobile", width: 120 },
{ title: "설명", field: "Description", width: 220 }
]]
});
},
selected() {
return $("#dgUser").datagrid("getSelected");
}
};
function formatEnabled(value) {
return value
? '<img src="/css/icon/ok.png" alt="활성" title="활성" />'
: '<img src="/css/icon/stop.png" alt="비활성" title="비활성" />';
}
4. 등록·수정·삭제·비밀번호 재설정
버튼 클릭 이벤트는 초기화 시 한 번만 등록하며, 각 기능은 공통 다이얼로그를 재사용한다. EasyUI의 form('load', row)를 활용하면 기존 데이터를 간단히 매핑할 수 있다.
function bindToolbarEvents() {
$("#btnAdd").click(() => openUserEditor("add"));
$("#btnEdit").click(() => openUserEditor("edit"));
$("#btnDelete").click(removeUser);
$("#btnResetPwd").click(resetPassword);
$("#btnRefresh").click(() => $("#dgUser").datagrid("reload"));
}
function openUserEditor(mode) {
const row = mode === "edit" ? userGrid.selected() : null;
if (mode === "edit" && !row) {
$.messager.alert("알림", "수정할 사용자를 선택하세요.", "warning");
return;
}
if (mode === "edit" && row.UserName === "Administrator" && currentUser.userName !== "Administrator") {
$.messager.alert("경고", "최고 관리자 계정은 변경할 수 없습니다.", "warning");
return;
}
$("#winUser").html(userFormHtml).dialog({
title: mode === "add" ? "사용자 등록" : "사용자 수정",
width: 640,
height: 520,
modal: true,
buttons: [
{
text: "저장",
iconCls: "icon-save",
handler: () => submitUser(mode, row ? row.Id : null)
},
{
text: "닫기",
handler: () => $("#winUser").dialog("close")
}
],
onOpen() {
lookupUtil.bindGender("cmbGender");
lookupUtil.bindRole("cmbRole");
lookupUtil.bindCompany("cmbCompany");
lookupUtil.bindDepartment("cmbDepartment");
lookupUtil.bindWorkgroup("cmbWorkgroup");
$("#uiform input[required], #uiform input[validType]").validatebox();
if (mode === "edit" && row) {
$("#uiform").form("load", row);
$("#cmbRole").combobox("setValue", row.RoleId);
$("#cmbCompany").combobox("setValue", row.CompanyId);
$("#cmbDepartment").combobox("setValue", row.DepartmentId);
$("#cmbWorkgroup").combobox("setValue", row.WorkgroupId);
} else {
$("#chkEnabled").prop("checked", true);
$("#txtUserName").focus();
}
}
});
}
function submitUser(mode, id) {
if (!$("#uiform").form("validate")) return;
const orgParams = collectOrgParams();
const action = mode === "edit" ? `action=edit&id=${id}` : "action=add";
const data = $("#uiform").serialize() + "&" + action + "&" + orgParams;
$.ajax({
url: "handler/UserHandler.ashx",
type: "POST",
data,
success(response) {
if (response === "1") {
$("#winUser").dialog("close");
$("#dgUser").datagrid("reload");
} else if (response === "0") {
$.messager.alert("알림", "이미 사용 중인 아이디입니다.", "warning");
$("#txtUserName").select();
} else {
$.messager.alert("오류", response, "error");
}
}
});
}
function collectOrgParams() {
return [
`roleId=${$("#cmbRole").combobox("getValue")}`,
`companyId=${$("#cmbCompany").combobox("getValue")}`,
`deptId=${$("#cmbDepartment").combobox("getValue")}`,
`wgId=${$("#cmbWorkgroup").combobox("getValue")}`,
`companyName=${encodeURIComponent($("#cmbCompany").combobox("getText"))}`,
`deptName=${encodeURIComponent($("#cmbDepartment").combobox("getText"))}`,
`wgName=${encodeURIComponent($("#cmbWorkgroup").combobox("getText"))}`
].join("&");
}
function removeUser() {
const row = userGrid.selected();
if (!row) {
$.messager.alert("알림", "삭제할 사용자를 선택하세요.", "warning");
return;
}
if (row.Id === currentUser.id) {
$.messager.alert("경고", "현재 로그인한 사용자는 제할 수 없습니다.", "warning");
return;
}
if (row.UserName === "Administrator") {
$.messager.alert("경고", "최고 관리자 계정은 삭제할 수 없습니다.", "warning");
return;
}
$.messager.confirm("확인", "선택한 사용자를 삭제하시겠습니까?", yes => {
if (!yes) return;
$.post("handler/UserHandler.ashx", { action: "delete", id: row.Id }, res => {
if (res === "1") {
$("#dgUser").datagrid("reload");
} else {
$.messager.alert("오류", res, "error");
}
});
});
}
function resetPassword() {
const row = userGrid.selected();
if (!row) {
$.messager.alert("알림", "비밀번호를 변경할 사용자를 선택하세요.", "warning");
return;
}
$("#winPwd").html(pwdFormHtml).dialog({
title: "비밀번호 재설정",
width: 320,
height: 180,
modal: true,
buttons: [{
text: "저장",
handler() {
if (!$("#txtNewPwd").validatebox("isValid")) return;
const data = {
action: "setpassword",
id: row.Id,
password: $("#txtNewPwd").val()
};
$.post("handler/UserHandler.ashx", data, res => {
if (res === "1") {
$("#winPwd").dialog("close");
$.messager.show({ title: "알림", msg: "비밀번호가 변경되었습니다." });
} else {
$.messager.alert("오류", res, "error");
}
});
}
}]
});
$("#loginDisplay").text(`${row.UserName} / ${row.RealName}`);
$("#txtNewPwd").validatebox();
}
5. 보 데이터 동적 바인딩
성별, 역할, 회사, 부서, 작업그룹 등은 별도 핸들러에서 조회한 후 EasyUI ComboBox로 바인딩한다. 이 로직은 lookupUtil 객체로 분리해 재사용성을 높인다.
const lookupUtil = {
bindCombo(ctrlId, url, valueField, textField) {
$(`#${ctrlId}`).combobox({
url,
valueField,
textField,
editable: false,
panelHeight: "auto"
});
},
bindGender(ctrlId) {
this.bindCombo(ctrlId, "handler/DataItemHandler.ashx?action=GetCategory&code=Gender", "ItemValue", "ItemName");
},
bindRole(ctrlId) {
this.bindCombo(ctrlId, "handler/RoleHandler.ashx?action=GetEnabledRoles", "Id", "RealName");
},
bindCompany(ctrlId) {
this.bindCombo(ctrlId, "handler/OrgHandler.ashx?action=GetOrg&category=Company", "Id", "FullName");
},
bindDepartment(ctrlId) {
this.bindCombo(ctrlId, "handler/OrgHandler.ashx?action=GetOrg&category=Department", "Id", "FullName");
},
bindWorkgroup(ctrlId) {
this.bindCombo(ctrlId, "handler/OrgHandler.ashx?action=GetOrg&category=Workgroup", "Id", "FullName");
}
};
6. 폼 템플릿
동적으로 생성하는 입력 폼은 HTML 문자열로 관리하며, 다이얼로그 오픈 시점에 테이너에 주입한다. 이 방식은 서버와의 더링 분리를 유연하게 한다.
const userFormHtml = `
<form id="uiform">
<table class="grid2" width="100%" cellpadding="5" cellspacing="0">
<tr>
<td align="right">아이디</td>
<td><input name="UserName" id="txtUserName" required="true" validType="length[2,40]" class="txt03"/></td>
<td align="right">이름</td>
<td><input name="RealName" id="txtRealName" required="true" validType="length[2,40]" class="txt03"/></td>
</tr>
<tr>
<td align="right">성별</td>
<td><input name="Gender" id="cmbGender" required="true" class="txt03"/></td>
<td align="right">연락처</td>
<td><input name="Mobile" id="txtMobile" class="txt03"/></td>
</tr>
<tr>
<td align="right">역할</td>
<td><input name="RoleId" id="cmbRole" required="true" class="txt03"/></td>
<td align="right">활성여부</td>
<td><input id="chkEnabled" type="checkbox" name="Enabled" checked="checked"/> 활성</td>
</tr>
<tr>
<td align="right">회사</td>
<td><input id="cmbCompany" class="txt03"/></td>
<td align="right">부서</td>
<td><input id="cmbDepartment" class="txt03"/></td>
</tr>
<tr>
<td align="right">작업그</td>
<td colspan="3"><input id="cmbWorkgroup" class="txt03"/></td>
</tr>
<tr>
<td align="right">설명</td>
<td colspan="3"><textarea name="Description" id="txtDescription" rows="3" class="txt03" style="width:470px;"></textarea></td>
</tr>
</table>
</form>`;
const pwdFormHtml = `
<table class="grid" id="pwdForm" width="100%">
<tr>
<td>사용자</td>
<td><span id="loginDisplay"></span></td>
</tr>
<tr>
<td>새 비밀번호</td>
<td><input id="txtNewPwd" name="password" type="password" required="true" validType="safepass" class="txt03"/></td>
</tr>
</table>`;
7. 초기화 및 주의사항
페이지 로드 시점에 그리드와 이벤트를 초기화하고, 권한이 없는 버은 클릭 이벤트에서 추가로 차단한다.
$(function () {
userGrid.init();
bindToolbarEvents();
});
- 최고 관리자 계정과 현재 로그인 사용자 계정에 대한 삭제/수정 제한은 클라이언트에서 한 번, 서버에서 다시 한 번 검증해야 한다.
- 사용자 삭제는 리 삭제(DelFlag)로 처리하며, 실수로 제한 경우 DB에서 플래그를 복원할 수 있다.
- 비밀번호 변경 시 서버에서도 복잡도 규과 해시 처리를 적용해야 한다.