login register Sysop! about ME  
qrcode
    최초 작성일 :    2003년 09월 29일
  최종 수정일 :    2003년 09월 29일
  작성자 :    taeyo
  편집자 :    Taeyo (김 태영)
  읽음수 :    63,883

강좌 목록으로 돌아가기

필자의 잡담~

이번에도 예상대로 늦었죠??? 사람인지라 가끔은 재난도 닥치고, 의외의 상황도 마주하게 되곤 하지요... 그래서, 잠시 늦었을 뿐... 그래도 포기하지는 않는답니다. 하하하 ^^

대상 : ASP.NET을 이용하여 스스로 일반 게시판이 작성가능하거나,
         Taeyo's ASP.NET v1.0 서적을 통해서 게시판 만들기를 이미 공부하신 분

저번 강좌를 통해서 게시물의 목록을 출력하는 것까지 완성해 보았습니다. 물론, 페이징도 되어있지 않고, 여러가지 추가적으로 손봐야 할 데가 많긴 하지만 말입니다. (그것들도 머지않아 같이 해볼 것입니다)

그러한 부분들은 일단 전체적인 게시판 구조를 완성한 다음에, 하나씩 잡아나가 보도록 하구요 ^^. 이번 강좌에서는 글의 상세내역을 확인할 수 있는 Content.aspx 페이지를 만들어 보도록 하겠습니다. 그리고, 글의 변경과 삭제, 답변 기능을 달 수 있는 기반(?)까지 만들어 보려 합니다. ^^

그럼 시작해 볼까요???

먼저, Content.aspx 페이지를 위해서 제일 먼저 필요한 것은 무엇일까요? 그렇습니다. 지정된 글의 상세내역을 가져오는 저장 프로시저가 필요할 것입니다. 그렇죠???  해서, 만들어 봅니다. 저장 프로시저 ^^ 다음과 같이 말이죠~~~

CREATE PROC UP_SELECT_BOARDCONTENT
    @seqint
AS
    SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
    SET NOCOUNT ON

    SELECT seq, thread, depth, writer, email, title, mode, ip, readcount, transdate, content
    FROM ThreadBoard
    WHERE seq = @seq
GO

이번에도 쿼리는 매우 간단합니다. 단지, 특정 레코드에서(@seq에 해당하는 레코드) 거의 대부분의 컬럼 값을 가져오는 쿼리이니까요 ^^. 제 경우는 pwd 컬럼을 제외한 모든 컬럼의 값을 다 가져오고 있는데요. 사실, 이 중에는 상황에 따라서 불필요한 컬럼이 있을 수도 있으니 여러분은 불필요한 컬럼을 잘 파악하셔서 그것들은 제외시키는 쪽으로 작성하시기 바랍니다.^^

이제, 프로시저를 위와 같이 작성하셨다면, [쿼리 분석기]로 이를 실행해 주세요~~

그러면, 뚜~둥 하면서.... UP_SELECT_BOARDCONTENT 프로시저가 만들어질 것입니다. 그렇다면, 이제 이 프로시저를 사용하는 Content.aspx 페이지를 작성해 보도록 할까요???

사실 Content.aspx 페이지의 디자인은 Insert.aspx 페이지의 디자인과 사뭇 흡사합니다. 입력을 목적으로 하는 페이지가 아니라 출력을 목적으로 하는 페이지라는 점이 차이이긴 하지만요. 디자인이 동일하다는 의미는 아닙니다. 뭔가 전체적인 레이아웃이 비슷하다는 의미인 것입니다. 하하하...   괜한 말은 한 것인가요?? 하하하... 무언가 멋적인 듯한... 하하하

일단, UI의 디자인을 보여드리도록 하겠습니다. 크게 이쁜 디자인은 아닙니다만...  나름대로는 깔끔하다고 생각합니다. 차후 제대로 된 디자인을 입힐 경우에 복잡해지지 않도록 하기 위해서 코드도 그다지 많지 않죠~~~ 네. 그렇습니다. 대략 다음과 같이 UI를 구성해 보았습니다.

Content.aspx 페이지의 디자인이 반드시 위와 같아야만 하는 것은 아닙니다. 아시겠지만, 여러분이 원하는 모습으로 작성하셔도 무방하지요. 컨트롤의 이름들만 맞춰주신다면 말입니다....  뭐 어쨌든 저는 이렇게 작성해 보았구요. 해서, 위의 UI를 기반으로 작성된 Content.aspx 페이지의 HTML은 다음과 같습니다. ^^;(
컨트롤들의 경우는 폰트 색상을 달리 해 보았으니 참고해 주세요 ^^)

<%@ Page language="c#" Codebehind="Content.aspx.cs" AutoEventWireup="false" Inherits="AspNetTest.ThreadBoard.contents" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
    <HEAD>
        <title>Content</title>
        <LINK href="css/main.css" type="text/css" rel="stylesheet">
    </HEAD>
    <body MS_POSITIONING="FlowLayout">
        <form id="Content" method="post" runat="server">
            <table cellSpacing="0" cellPadding="0" width="520" border="0">
                <tr>
                    <td bgColor="#336699"></td>
                </tr>
                <tr>
                    <td bgColor="#668fcd"></td>
                </tr>
            </table>
            <table cellSpacing="2" cellPadding="0" width="520" border="0">
                <tr class="txt01">
                    <td height="25">  글 내용보기</td>
                </tr>
            </table>
            <table cellSpacing="0" cellPadding="0" width="520" border="0">
                <tr class="txt01">
                    <td bgColor="#f0f0f0"></td>
                </tr>
                <tr class="txt01">
                    <td bgColor="#a6a6a6"></td>
                </tr>
                <tr class="txt01">
                    <td bgColor="#dadada" height="2"></td>
                </tr>
            </table>
            <table cellSpacing="0" cellPadding="5" width="520" border="0">
                <tr class="txt01">
                    <td width="120">이름</td>
                    <td width="400">
                        <asp:Label id="lblName" runat="server"></asp:Label></td>
                </tr>
                <tr height="1">
                    <td background="images/dotline.gif" colSpan="2"></td>
                </tr>
                <tr class="txt01">
                    <td>작성일</td>
                    <td>
                            <asp:Label id="lblDate" runat="server"></asp:Label></td>
                </tr>
                <tr height="1">
                    <td background="images/dotline.gif" colSpan="2"></td>
                </tr>
                <tr class="txt01">
                    <td>읽음수</td>
                    <td>
                        <asp:Label id="lblRead" runat="server"></asp:Label></td>
                </tr>
                <tr height="1">
                    <td background="images/dotline.gif" colSpan="2"></td>
                </tr>
                <tr class="txt01">
                    <td>제목</td>
                    <td>
                        <asp:Label id="lblTitle" runat="server"></asp:Label></td>
                </tr>
                <tr height="1">
                    <td background="images/dotline.gif" colSpan="2"></td>
                </tr>
                <tr class="txt01" bgcolor="#f0f0f0">
                    <td colSpan="2">내용
                    </td>
                </tr>
                <tr class="txt01">
                    <td colSpan="2">
                        <asp:Label id="lblContent" runat="server"></asp:Label></td>
                </tr>
                <tr height="1">
                    <td colSpan="2" bgColor="#dadada" height="1"></td>
                </tr>
                <tr>
                    <td align="right" colSpan="2">
                        <asp:imagebutton id="btnList" runat="server" AlternateText="리스트로"
                         ImageUrl="images/b_list.gif" ImageAlign="AbsBottom"
                         CausesValidation="False"></asp:imagebutton>

                        <asp:imagebutton id="btnReply" runat="server" AlternateText="답글달기"
                         ImageUrl="images/b_reply.gif" ImageAlign="AbsBottom"
                         CausesValidation="False"></asp:imagebutton>

                        <asp:TextBox Runat="server" ID="pwd" CssClass="input"
                         TextMode="Password"></asp:TextBox>

                        <asp:RequiredFieldValidator id="RequiredFieldValidator1" runat="server"
                         ErrorMessage="비밀번호를 입력해 주세요" ControlToValidate="pwd"
                         Display="None"></asp:RequiredFieldValidator>

                        <asp:imagebutton id="btnEdit" runat="server" AlternateText="수정하기"
                    ImageUrl="images/b_edit.gif" ImageAlign="AbsBottom"></asp:imagebutton>

                        <asp:imagebutton id="btnDelete" runat="server" AlternateText="삭제하기"
                  ImageUrl="images/b_delete.gif" ImageAlign="AbsBottom"></asp:imagebutton>

                    </td>
                </tr>
            </table>
            <asp:Label id="lblError" runat="server" CssClass="txt01" ForeColor="Red"
             BorderWidth="1px" BorderStyle="Solid" BorderColor="Red" Width="520px"
             Visible="False"></asp:Label>
<br>
            <asp:ValidationSummary id="ValidationSummary1" runat="server"
             ShowMessageBox="True" ShowSummary="False" DisplayMode="List">
            </asp:ValidationSummary>

        </form>
    </body>
</HTML>

소스의 윗쪽에 나열된 Labe 컨트롤들은 각각 글의 작성자와 날짜, 읽음 수, 본문 내용들을 출력하기 위한 컨트롤입니다. 반면, 밑에 쪽에 놓인 Button 컨트롤들은 각각 [리스트로 이동], [답변하기], [수정하기], [삭제하기] 등의 작업을 위한 버튼들이지요 ^^ 또한, 수정 및 삭제 작업을 위해서는 글의 비밀번호를 확인할 필요가 있기에, 글의 비밀번호를 입력하는 TextBox(id : pwd)도 하나 놓여져 있습니다. 그리고, 이 텍스트박스는 수정 및 삭제 버튼이 눌릴 경우에는 필수 입력되어져야만 하는 컨트롤이기에, RequiredFieldValidator 컨트롤을 사용하여 pwd 텍스트박스를 검사하도록 하고 있구요. ^^

이렇게 하단에 놓여진 컨트롤들에 대해서는 잠시 후에 설명하기로 하구요. 일단은 list.aspx 페이지에서 넘어오는 seq 값을 가지고, Content.aspx 페이지를 꾸미는 부분부터 설명하도록 하겠습니다.

이를 이해서는 코드 비하인드 페이지의 Page_Load 이벤트 구역을 다음과 같이 작성할 필요가 있을 것입니다. ^^;

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;

namespace AspNetTest.ThreadBoard
{
    /// <summary>
    /// Content에 대한 요약 설명입니다.
    /// </summary>
    public class contents : System.Web.UI.Page
    {
        protected System.Web.UI.WebControls.ImageButton btnReply;
        protected System.Web.UI.WebControls.ImageButton btnEdit;
        protected System.Web.UI.WebControls.ImageButton btnDelete;
        protected System.Web.UI.WebControls.Label lblName;
        protected System.Web.UI.WebControls.Label lblDate;
        protected System.Web.UI.WebControls.Label lblRead;
        protected System.Web.UI.WebControls.Label lblTitle;
        protected System.Web.UI.WebControls.Label lblContent;
        protected System.Web.UI.WebControls.Label lblError;
        protected System.Web.UI.WebControls.ImageButton btnList;
        protected System.Web.UI.WebControls.TextBox pwd;
        protected System.Web.UI.WebControls.RequiredFieldValidator RequiredFieldValidator1;
        protected System.Web.UI.WebControls.ValidationSummary ValidationSummary1;
    
        private string seq = string.Empty;

        private void Page_Load(object sender, System.EventArgs e)
        {
            // 여기에 사용자 코드를 배치하여 페이지를 초기화합니다.
            if(!IsPostBack)
            {
                if(Request.Params["seq"] != null)
                    seq = Request.Params["seq"].ToString();

                if(seq == string.Empty)
                {
                    Response.Redirect("list.aspx");
                    Response.End();
                }

                getContent();
            }
        }

        private void getContent()
        {
            string ConnectStr = "server=(local);database=TEST;user id=sa";

            SqlConnection Con = new SqlConnection(ConnectStr);
            SqlCommand Cmd = new SqlCommand();
            Cmd.Connection = Con;

            Cmd.CommandType = CommandType.StoredProcedure;

            Cmd.Parameters.Add("@seq", SqlDbType.Int);
            Cmd.Parameters["@seq"].Value = seq;

            try
            {
                Con.Open();

                //레코드를 읽어오는 프로시저로 지정
                Cmd.CommandText = "UP_SELECT_BOARDCONTENT";

                SqlDataReader reader = Cmd.ExecuteReader(CommandBehavior.CloseConnection);
                if(reader.Read())
                {
                    //seq, thread, depth, writer, email, title, mode, ip, readcount, transdate, content
                    ViewState["seq"] = reader[0].ToString();
                    ViewState["thread"] = reader[1].ToString();
                    ViewState["depth"] = reader[2].ToString();

                    lblName.Text = reader[3].ToString();
                    lblTitle.Text = reader[5].ToString();
                    lblRead.Text = reader[8].ToString();
                    lblDate.Text = reader[9].ToString();

                    bool mode = (bool)reader[6];
                    if(mode)
                    {
                        lblContent.Text = ReplaceBR(reader[10].ToString());
                    }
                    else
                    {
                        lblContent.Text = ReplaceBR(Server.HtmlEncode(reader[10].ToString()));
                    }
                }
                else
                {
                    string script;
                    script = "<script>alert('글이 존재하지 않습니다'); history.back();</script>";
                    Page.RegisterClientScriptBlock("done", script);
                }

                reader.Close();
            }
            catch(Exception ex)
            {
                lblError.Text = "ERROR : " + ex.Source + " - " + ex.Message;
                lblError.Visible = true;
                
            }

            if(Con.State == ConnectionState.Open)
                Con.Close();

            Cmd = null;
            Con = null;

        }

        protected string ReplaceBR(string s)
        {
            return s.Replace("\n", "<BR>");
        }
    }
}

소스에서는 먼저 list.aspx 페이지에서 넘어온 값인, 사용자가 선택한 글의 seq 값을 받아서 저장하기 위한 클래스 변수를 선언하고 있습니다. 그리고, 그 값을 Empty 값으로 초기화하고 있지요

private string seq = string.Empty;

이 seq 라는 변수는 잠시후 Request 개체를 통해서 실제 seq 값으로 지정될 것입니다. ^^ 그리고, 그 값을 가지고 데이터베이스로 쿼리를 날리게 될 것이지요 ^^

이제, Page_Load 쪽을 보도록 하겠습니다. 그 안에서는 먼저, IsPostBack 여부를 확인하고 있습니다. 즉, 페이지가 처음 로드되는 경우에만 이후의 작업을 진행하도록 하겠다는 의미인 것이지요 ^^. 데이터베이스로부터 가져올 글의 내용들은 각각 Label 컨트롤로 표현될 것이고, 그 Label에 출력되는 텍스트들은 자동으로 그 상태가 유지될 것이기에, 데이터베이스로부터 데이터를 가져오는 작업은 페이지가 처음 로드되는 시점에만 수행해주면 될 것입니다.  포스트백 시에는 ASP.NET이 ViewState를 사용하여 알아서 컨트롤들의 값을 자동으로 채워줄테니까요 ^^

해서, Page_Load 이벤트 안에서는 먼저 현재가 페이지가 처음 로드되는 시점인지를 검사하고 있구요. 만일 그렇다면(즉, !IsPostBack) 이후의 내용을 진행하고 있습니다. 이후의 내용을 이제 살펴볼까요???

if(Request.Params["seq"] != null)
    seq = Request.Params["seq"].ToString();

그렇습니다. 페이지가 처음 로드되는 경우라면 먼저, list.aspx 페이지로부터 넘어오는 seq 라는 값이 있는지를 검사합니다. Content.aspx 페이지는 자기 홀로 동작할 수 있는 페이지가 아니라, 반드시 list.aspx 페이지에서 seq 값을 넘겨 받아야지만 동작할 수 있는 페이지니까요 ^^ 해서, 소스에서는 Request.Params을 사용하여(Request.QueryString을 사용해도 무관합니다) list.aspx 페이지로부터 넘어오는 seq 값을 얻어오고 있습니다. ^^  만일, 그러한 값이 넘어오고 있다면 말이지요

하지만, 성격이 이상한 사람들은 이상한 목적을 가지고 Content.aspx 페이지로 바로 접근할 수도 있습니다. 물론, 그런 경우가 아주 많지는 않겠지만, 간혹 그런 이상한 사람들이 있곤 하죠. 해서, 그러한 경우에는 친절하게(?) 그 사람들을 list.aspx 페이지로 돌려보내 주는 것이 좋겠죠?? 해서, 이어지는 코드는 바로 그러한 작업을 위한 코드입니다.

if(seq == string.Empty)
{
    Response.Redirect("list.aspx");
    Response.End();
}

만일, seq 변수의 값이  string.Empty라면, 이 사람은 정상적인 방법을 통해서 Content.aspx 페이지로 접근한 것이 아니니까요.(즉, list.aspx 페이지를 거치치 않고 요상하게 접근한 것이죠) 가차없이 list.aspx 페이지로 분기 시켜주면 되는 것이랍니다. ^^

해서, 여기까지의 코드로 우리는 seq 값이 확실히 있는 경우만 이하의 코드를 진행할 수 있다고 보장할 수 있게 되는 것이지요 ^^

그럼, 이제는 어떤 작업을 진행해야 할까요? 그렇습니다. 이제 이 seq 값을 가지고 데이터베이스에 쿼리를 날려서 필요한 레코드를 가져와야 할 것입니다. ^^ 그리고, 그 내용들을 각각의 Label 컨트롤에 출력해주어야 하겠죠???

해서, 이어지는 코드는 getContent() 라는 함수를 호출하고 있습니다. getContent()라고 별도로 만들어 둔 함수를 말이지요. 물론, 여러분들은 getContent() 라는 메서드가 데이터베이스로부터 데이터를 가져와서 각각의 Label 컨트롤에 출력하는 역할을 할 것이다라고 추측할 수 있을 것입니다. 그리고, 그 예상은 아주 정확할 수 밖에 없네요~~~ 하하하

그렇다면, 이제 getContent 구역을 살펴볼까요??? 그 안의 코드는 데이터베이스를 연결하기 위한 준비작업이 한창입니다. 자주 봐왔던 코드들이지요? 그렇습니다. Connection 개체를 구성하고, Command 개체를 준비하고 있죠. 그리고, 또한 Command 개체에는 다음과 같이 seq 매개변수도 설정하고 있답니다.

Cmd.Parameters.Add("@seq", SqlDbType.Int);
Cmd.Parameters["@seq"].Value = seq;

이러한 모든 설정 작업은 가급적 데이터베이스를 오픈하기 전에 하시는 것이 바람직 합니다. 해서, 코드에서도 그렇게 작성되어져 있죠? Open()은 명령을 실행하기 바로 직전에 해주시는 것이 성능 측면에서 좋습니다. 기억해 주세염~~ 방가방가~~

이제, 코드는 try 구역으로 들어갑니다. 묵시적인 빤쓰 선전이 아닙니다. 이거~~  츄라이~~~  빤쓰 선전이 아니구요. 예외처리 구역입니다. 이미 아시고 계시겠지만 말입니다. -_-+  빤쑤는 역시 보뒤가두~~

자. 츄라이 구역에서는 먼저 데이터베이스를 연결하는 Open 메서드가 날려지고 있습니다. 호곡~ 멋지네요

Cmd.CommandText = "UP_SELECT_BOARDCONTENT";
SqlDataReader reader = Cmd.ExecuteReader(CommandBehavior.CloseConnection);
if(reader.Read())
{

그리고, 우리가 이 강좌를 시작하면서 작성해 둔 프로시저인 UP_SELECT_BOARDCONTENT를 CommandText로 지정하고 있구요. ExecuteReader 메서드를 사용하여 그 프로시저를 수행한 결과를 SqlReader 에 담고 있습니다. 거의 공식과 같은 순서로 진행되고 있습지요. 게다가, 나중에 모든 작업이 끝난 다음에 Reader를 Close 하면, 따라서 Connection도 자동으로 Close 되게 하려고 ExecuteReader 메서드에 CommandBehavior.CloseConnection 인자를 사용하고 있는 것도 볼 수 있습니다. 정말로 잘 짜여진 코드가 아닐 수 없네요 ^^;;; (헉!!  -_-+)

그리고, SqlReader 클래스의 Read() 메서드를 사용합니다. 이 메서드를 사용하면 SqlReader 개체가 레코드를 가지고 있는지 여부를 알아낼 수 있습니다. 해서, 만일, SqlReader가 레코드를 가지고 있다면 true를, 그렇지 않다면 false를 넘겨주는 것이죠. 그리고, 이 메서드는 동시에 실제 첫번째 레코드로 포인터를 이동시키는 역할도 합니다.

해서, SqlReader를 이용해서 데이터베이스로부터 데이터를 가져온 경우에는 반드시 실제 레코드들에 접근하기 전에, 레코드가 존재하는 지 검사하고, 존재한다면 실제 첫번째 레코드로 접근하기 위해서 Read() 메서드를 사용해야 합니다. 이를 통해서, 개발자는 레코드가 있는 경우와 레코드가 없는 경우를 모두 각각 처리할 수가 있게 되는 것이지요. 다음처럼 말입니다

if(reader.Read())
{
    //레코드가 존재하면, 현재 첫번째 레코드인 상태이다.
    //현재의 레코드에서 필요한 컬럼에 접근할 수 있다.

}
else
{
    //레코드가 존재하지 않는다.
    //검색 결과가 없다는 메시지를 사용자에게 출력해주는 것이 좋다

}

그럼 먼저 레코드가 존재하는 경우를 살펴보도록 하겠습니다. 즉, if 문 구역을 말하는 것이겠죠? 그 안에서는 예상한대로 현재 레코드의 각각의 컬럼 값을 가져와서 Label에 지정하고 있습니다. 다음과 같이 말이죠

//seq, thread, depth, writer, email, title, mode, ip, readcount, transdate, content
ViewState["seq"] = reader[0].ToString();
ViewState["thread"] = reader[1].ToString();
ViewState["depth"] = reader[2].ToString();

lblName.Text = reader[3].ToString();
lblTitle.Text = reader[5].ToString();
lblRead.Text = reader[8].ToString();
lblDate.Text = reader[9].ToString();

bool mode = (bool)reader[6];
if(mode) {
    lblContent.Text = ReplaceBR(reader[10].ToString());
}
else {
    lblContent.Text = ReplaceBR(Server.HtmlEncode(reader[10].ToString()));
}

소스를 잘 보시면, 컬럼 값들을 Label에 지정하는 작업뿐 아니라, 몇몇 컬럼은 ViewState에 저장하고도 있는 것을 보실 수 있을 것입니다. ViewState가 페이지의 포스트백 사이에 정보를 유지하기 위한 수단인 것은 아시죠? 그렇다면, 왜 seq, thread, depth 값들을 뷰 상태에 저장하는 것일까요??? 그것은!!!  나중에 [답변하기]를 하게 될 경우에 Insert.aspx 페이지로 이 값들을 넘겨주기 위해서 이 컬럼의 값들이 어딘가에 저장해두고 있어야 하기 때문입니다. 그래야, [답변하기] 버튼을 누를 경우에 그러한 값들을 Insert.aspx에게로 Redirect하면서 같이 넘겨줄 수가 있으니까요 ^^. 예전에는 같은 처리를 위해서 Hidden 컨트롤을 사용했었습니다만... ViewState도 사실 내부적으로는 Hidden 컨트롤을 이용하니깐... 뭐 비슷한 방식이라고 볼 수도 있겠네요. 비슷하지만, 개발자가 훨씬 사용하기 편하고, 좀 더 보안적이기도 하구요(데이터가 Base64 인코딩되어 저장되니까요^^)

정리하자면, 답변 기능에 필요한 3개의 컬럼(seq, thread, depth) 값은 ViewState에 저장하고, 그 외의 컬럼은 출력을 위해서 Label 컨트롤에 지정되고 있는 것입니다.

그런데, mode 라는 컬럼에 대해서는 약간의 추가적인 처리도 해 주고 있습니다. mode 컬럼은 사용자가 HTML의 사용여부를 지정하는 컬럼이잖아요??? ^^ 그러니깐, 그 지정에 따라 본문내용이 HTML 반영되서 나오든가, HTML 반영되지 않은 채로 나오든가 해야 한다는 것입니다. 기본적으로 데이터베이스에 들어가 있는 데이터를 그대로 Label에 출력해 주면 그 상태 자체는 HTML 반영되는 상태입니다. 해서, mode 값이 true 인 경우는 위의 소스에서 보이다시피 그냥 컬럼의 값을 그대로 출력해주고 있고(ReplaceBR 함수에 대해서는 잠시 후에 설명드립니다), false인 경우는 Server.HtmlEncode() 메서드를 통해서 여러가지 HTML 태그들을 그에 상응하는 익스케이프 코드로 바꾸어 출력하고 있는 것입니다. Server.HtmlEncode() 메서드는 여러가지 HTML 태그들을 적절한 대체기호로 바꾸어주는 역할을 하니까요. 예를 들면, <는 &lt;로 바꾸어주고, >는 &gt; 로 바꾸어주는 것이지요 ^^

이렇게 해주면, mode 값이 무엇이냐에 따라 HTML 태그가 적용된 결과나 그렇지 않은 결과를 얻을 수가 있게 되는 것입니다. ^^ 그렇다면, ReplaceBR 이라는 함수는 무엇이냐? 이것은 제가 그냥(!) 만든 함수인데요. HTML은 엔터코드라는 것을 인식하지 못하기 때문에요. 결과 HTML 내용이 엔터처리가 된 상태로 나타나기 위해서는 엔터코드 대신에 HTML이 인식할 수 있는 코드, <br>을 사용해 주어야 하거든요. 해서, 그렇게 동작하게끔... 컬럼 값 안에 포함된 모든 엔터코드(\n)를 <br>이라는 문자로 Replace 해주는 함수가 바로 그겁니다. 해서, ReplaceBR 함수의 내용을 보시면 다음과 같지요. 무지 간단함다!!

protected string ReplaceBR(string s)
{
    return s.Replace("\n", "<BR>");
}

단순합니다. 그냥 Replace를 해서 변환된 문자열을 리턴하는게 전부입니다. 물론, string이 아닌 stringBuilder를 사용하는게 성능면에서 낫지 않나하는 생각도 듭니다만... 지금의 경우는 그리 큰 차이는 없을 것이기에, 가벼운 마음으로 작성해 보았습니다.

자. 여기까지가 레코드가 존재할 경우, 그 컬럼 값들을 화면에 출력하는 코드였습니다. 말만 길었지 사실 내용은 그리 어렵지 않았을 거예요. 제가 좀 말이 많아서인지.... 괜히 설명만 길었던 것 같기도 하네요 ^^

그 이후의 코드는 레코드가 없을 경우에는 클라이언트 스크립트를 사용해서 글이 존재하지 않는다는 메시지를 사용자에게 날리면서, list.aspx 페이지로 돌아가도록 하고 있습니다.

그리고, 이제 이어지는 코드는 데이터베이스 관련 개체들을 모두 해제하는 것으로 마무리 되고 있습니다. ^^

조금 복잡한 듯도 하고, 조금 설명이 부족한 듯도 해 보입니다만.... ^^; 일단 현재까지의 결과를 확인해 보도록 하세요 ^^ 대략 다음과 같은 유사한 결과를 보실 수 있을 것입니다. ^^

잘 동작하죠? 그런데 문제가 하나 있습니다. 일단, 첫번째 문제는 조회수가 증가하지 않는다는 것입니다. 그렇죠? 현재는 말입니다..  이를 해결하기 위해서는 일단 현재 글의 본문내용을 가져오는 데이터베이스 쿼리를 실행하기 이전에, 먼저 현재 글의 조회수를 증가시키는 쿼리를 먼저 실행해 주어야 할 것입니다. 그렇다면, 조회수를 증가시키는 쿼리는 어떻게 되나요? 그것도 프로시저로 작성해야 하지 않을까요??? 그렇습니다... 그러면, 작성해야 하겠지요~~ 다음처럼요~~~

CREATE PROC UP_UPDATE_BOARDREADCOUNT
    @seq    int
AS
    SET NOCOUNT ON

    UPDATE ThreadBoard
    SET readcount = readcount + 1
    WHERE seq = @seq
GO

그리고, 이러한 프로시저를 실행하도록 코드 비하인드 쪽의 코드를 일부 수정해야 하겠지요?? getContent() 메서드 쪽을 다음과 같이 말입니다...

private void getContent()
{
    string ConnectStr = "server=(local);database=TEST;user id=sa";

    SqlConnection Con = new SqlConnection(ConnectStr);
    SqlCommand Cmd = new SqlCommand();
    Cmd.Connection = Con;

    Cmd.CommandType = CommandType.StoredProcedure;

    Cmd.Parameters.Add("@seq", SqlDbType.Int);
    Cmd.Parameters["@seq"].Value = seq;

    try
    {
        Con.Open();

        //읽음 수 증가!!
        Cmd.CommandText = "UP_UPDATE_BOARDREADCOUNT";
        Cmd.ExecuteNonQuery();


        //레코드를 읽어오는 프로시저로 지정
        Cmd.CommandText = "UP_SELECT_BOARDCONTENT";

        SqlDataReader reader = Cmd.ExecuteReader(CommandBehavior.CloseConnection);
        if(reader.Read())
        {
            ...             ... 이하는 기존과 같음 ...            ...

코드를 위와 같이 바꾸면 이제 읽음 수는 잘 증가할 것입니다. 그것도 매우 잘 말입지요~~~ ^^

하지만, 여전히 문제는 남아있습니다. 그것은 list.aspx 페이지에서 Content.aspx로 왔을 경우 조회수가 증가하기는 하지만, 현재 상태(Content.aspx 상태)에서 새로 고침을 해도 조회수가 증가한다는 것입니다. 흐음... 이래서는 안되지요!!!  새로 고침한다구 조회수가 증가해서야 되겠습니까???  해서, 우리는 이 부분을 개선하려 합니다. 즉, 반드시 list.aspx에서 Content.aspx로 접근해야만 조회수가 증가하게 하구요. 그외의 경우에는 조회수가 증가하지 않게 하려는 것이지요.

그렇다면, 어떻게 이러한 처리를 할 수 있을까요. 현재의 상태가 list.aspx로부터 온 상태인지, 다른 상태인지 알 길이 있을까요? 사실상 그것은 알아낼 수가 없는 쪽에 가깝습니다. Request.UrlReferer를 검사하면 이전에 거쳐온 페이지가 list.aspx 인지 아닌지 여부를 알 수는 있습니다만..  이 정보만으로는 현재의 상태가 list.aspx 페이지로부터 넘어온 상태인지 아니면, Content.aspx 페이지에서 새로고침을 한 상태인지를 알아낼 수는 없는 것이지요. 해서 제가 선택한 방법은 쿠키(Cookie) 입니다. 좀 더 똑똑하신 분들은 또 다른 방안을 생각해 내실 수도 있을 것입니다만... 저의 경우는 예전부터 많이 써오는 방법을 사용하여 이 문제를 해결해 볼까 합니다.

쿠키를 사용하여 이러한 문제를 해결하는 방법은 간단합니다. 일단, 어떤 쿠키의 값(예를 들면, 0)을 list.aspx 페이지에서 설정한 다음에, Content.aspx 페이지에서는 그 쿠키의 값이 지정된 값(예, 0)인 경우에만 조회수 증가 쿼리명령을 수행하고, 쿠키의 값을 다른 값(예, 1)으로 변경하는 것입니다. 그러면, 일단 한번 조회수가 증가된 다음에는 쿠키의 값이 변경되었기에, 아무리 페이지를 새로고침해도 다시는 조회수 증가 처리가 일어나지 않을 것입니다. 쿠키값이 0인 경우에만 조회수 증가 처리가 수행되는데, 현재의 쿠키값은 1이니까요 ^^. 조회수는 반드시 list.aspx 페이지가 로딩되어 쿠키값이 0으로 지정된 다음에 Content.aspx 페이지로 와야만 증가될 것입니다. ^^

자. 이런 식으로 페이지를 약간씩 수정해 보도록 하겠습니다. 먼저, list.aspx 페이지의 Page_load 구역에 다음과 같이 쿠키를 설정하는 코드를 추가합니다.

    private void Page_Load(object sender, System.EventArgs e)
    {
        // 여기에 사용자 코드를 배치하여 페이지를 초기화합니다.

        if(!IsPostBack)
        {
            Listing();
        }

        Response.Cookies["ThreadBoard"]["UpdateReadCount"] = "0";
    }

그리고, 이제 Content.aspx 페이지도 약간 손을 봐 주어야 합니다. 먼저, 쿠키값을 저장할 클래스 변수를 하나 선언하구요.(소스에서는 변수명을 updateReadCount로 선언하고 있습니다) 그 다음, Page_Load 이벤트 안에서 쿠키 값을 읽어와서 updateReadCount 변수에 저장합니다. 그리고, getContent 메서드 구역에서 조금 전에 추가한 코드를 다음과 같이 약간 손을 봅니다. 쿠키값이 0인 경우에만 조회수 증가 처리작업을 진행하도록 말입니다. ^^

... 이상은 기존과 같음 ...
...
private string seq = string.Empty;
private string updateReadCount = "0";

private void Page_Load(object sender, System.EventArgs e)
{
    // 여기에 사용자 코드를 배치하여 페이지를 초기화합니다.
    if(!IsPostBack)
    {
        if(Request.Params["seq"] != null)
            seq = Request.Params["seq"].ToString();

        if(Request.Cookies["ThreadBoard"] != null)
            updateReadCount = Request.Cookies["ThreadBoard"]["UpdateReadCount"];

        if(seq == string.Empty)
        {
            Response.Redirect("list.aspx");
            Response.End();
        }

        getContent();
    }
}

private void getContent()
{
    string ConnectStr = "server=(local);database=TEST;user id=sa";

    SqlConnection Con = new SqlConnection(ConnectStr);
    SqlCommand Cmd = new SqlCommand();
    Cmd.Connection = Con;

    Cmd.CommandType = CommandType.StoredProcedure;

    Cmd.Parameters.Add("@seq", SqlDbType.Int);
    Cmd.Parameters["@seq"].Value = seq;

    try
    {
        Con.Open();

        //읽음 수 증가!!
        if(updateReadCount == "0")
        {
            Cmd.CommandText = "UP_UPDATE_BOARDREADCOUNT";
            Cmd.ExecuteNonQuery();

            Response.Cookies["ThreadBoard"]["UpdateReadCount"] ="1";
        }

        //레코드를 읽어오는 프로시저로 지정
        Cmd.CommandText = "UP_SELECT_BOARDCONTENT";

        SqlDataReader reader = Cmd.ExecuteReader(CommandBehavior.CloseConnection);
        if(reader.Read())
        {
            ...             ... 이하는 기존과 같음 ...            ...

그다지 어렵지 않지요????  이 정도는 어려워서는 안될 듯 합니다요... 물론, 어렵게 느껴지는 분들도 있을 것입니다. 만일, 어렵게 느껴지시는 분들이 있다면 그 분들은 ASP.NET이 어려운 것이 아니라 웹 프로그램이 어려우신 것일 겁니다. 전체적인 웹의 방식에 익숙하지가 않아서 말이지요 ^^ 그리고, 그러한 문제는 시간이 해결해주는 문제이니 너무 걱정하지 마세요 ^^ 프로그래머는 딱 두가지만 조심하면 됩니다. 하나는 조급함이구요. 다른 하나는 자만입니다. ^^  제 생각이 그렇다는 것이지요... 뭐....

자. 이제 페이지를 테스트해 보세요... 제법 쓸만하게 동작하지 않나요??? ^^

"네... "

역시 그렇군요.. 다행입니다. ^^

그러면, 이제 Content.aspx 페이지에서 제공해야하는 부가 기능들에 대해서 이야기할 시간이 온 것 같네요.. 그러니깐, 굳이 이야기하자면, [리스트로 돌아가기], [답변하기], [수정하기], [삭제하기] 와 같은 것들에 대해서 말입니다. 이 기능들을 이 자리에서 모두 이야기하기에는 좀 강좌가 길어지는 버거움이 느껴질 것 같네요. 해서, 일단, 수정 및 삭제 기능 부터 시작해서 다음 강좌에서 다루어 볼까 합니다. ^^ 약속하겠습니다요.. 다음 번 강좌는 반드시 이번처럼 늦게 올라오지 않도록 하겠습니다. 일주일 이내에는 반드시 올라오도록 준비하겠습니다. (ㅠ_ㅠ)

자. 그럼 일단 믿음 한번 들어와 주시면서, 하단에 놓여진 버튼 컨트롤에 대해서 약간의 설명을 드리도록 하겠습니다. 그렇습니다. 하단의 버튼들은 하나같이 imagebutton 입니다. 보이다시피 첫번째 버튼은 [list] 이고, 두 번째 버튼은 [Reply]이지요. 그리고, 사이에 TextBox가 하나 놓여있구요. 그 우측으로 각각 [Edit]와 [Delete]가 위치하고 있습니다.

가운데에 위치한 TextBox는 비밀번호를 입력받는 컨트롤이구요. 이 컨트롤은 RequiredFieldValidator 컨트롤에 의해서 필수 입력 컨트롤로 동작하고 있습니다. 해서, 글을 변경(Edit)하거나 삭제(Delete)하려면 반드시 이 값을 입력해 주어야 하지요 ^^

그런데, 여기서 주의해야 할 부분이 하나 있습니다. 폼에 RequiredFieldValidator 와 같은 유효성 검사 컨트롤이 올라오게 되면요~ 폼이 포스트백 될 경우에는 언제나 이 컨트롤이 동작하여 유효성 검사를 실시하게 됩니다. 해서, 만일 폼의 유효성에 문제가 있다고 판단되면, 폼이 전송되지 않는 것이지요. 이러한 검사가 Edit와 Delete 시에는 확실히 필요한 작업입니다만... List나 Reply의 경우는 비밀번호가 반드시 필요한 경우는 아닙니다. 비밀번호를 몰라도 list 페이지로 이동하거나, 답변은 할 수가 있어야 하지 않겠습니까?

그래서, List와 Reply용 이미지버튼 컨트롤의 HTML을 보시면 다른 컨트롤과는 달리 CausesValidation라는 속성이 설정되어져 있는 것을 보실 수가 있을 것입니다.  CausesValidation="False" 라고 말이지요 ^^. 컨트롤에게  CausesValidation="False"와 같은 지정을 하게 되면 이 컨트롤이 포스트백을 일으킬 경우는 유효성 검사를 무시하도록 합니다. 해서, 이렇게 설정된 경우는 비밀번호가 입력되지 않아도 각각의 버튼이 동작을 할 수 있게 된다는 이야기인 것입니다. ^^

좋습니다. 그렇다면, 한번 List 이미지 버튼에 대한 코드를 작성해 볼까요??? VS.NET에서 해당 이미지를 더블 클릭하면 자동으로 코드 비하인드로 이동하면서 다음과 같은 Click 이벤트 코드가 만들어지지요? 그 안을 다음과 같이 채워보시기 바랍니다.

    private void btnList_Click(object sender, System.Web.UI.ImageClickEventArgs e)
    {
        Response.Redirect("list.aspx");
    }

단순하죠? 그럴 수 밖에요... List 버튼을 누르면, 지금은 단지 사용자를 List.aspx 페이지로 분기시켜 주기만 하면 되니까요.. 물론, 나중에 페이징 기능이 List.aspx 페이지에 추가되면, 이 코드도 List.aspx 페이지에게로 페이지 번호 값을 함께 전송해 주는 코드로 바뀌어야 할 것입니다만... 지금은  이 정도로도 충분할 것입니다. ^^

그럼... 이번에는 남은 3개의 버튼 중 어떤 것을 한번 작성해 볼까요?

어떤 것부터 했으면 좋겠어요????  이런~~ 이런 ~~ 이번 강좌의 스크롤바가 얼마남지 않은 것을 이미 보셨구만요... 그렇습니다... 이번 강좌는 여기까지 입니다.....  안타깝게도 말입니다. 하지만, 일주일만 기다리시면, 나머지 3개의 버튼을 하나씩 구현해 나가는 강좌가 속속 올라올 것입니다.

이번에는 절대 배신을 안 때릴 것이니 부디 기대해 주세요 ^^ 감사합니다.


authored by


 
 
.NET과 Java 동영상 기반의 교육사이트

로딩 중입니다...

서버 프레임워크 지원 : NeoDEEX
based on ASP.NET 3.5
Creative Commons License
{5}
{2} 읽음   :{3} ({4})