jQuery EasyUI 기반 관리 시스템 사용자 관리 모 구현

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에서 플래그를 복원할 수 있다.
  • 비밀번호 변경 시 서버에서도 복잡도 규과 해시 처리를 적용해야 한다.

태그: jQuery EasyUI ASP.NET C# DataGrid

9월 7일 19:43에 게시됨