대상 : ASP.NET을 이용하여 스스로 일반 게시판이 작성가능하거나,
Taeyo's ASP.NET v1.0 서적을 통해서 게시판 만들기를 이미 공부하신 분
안녕하세요? 태오입니다.
그렇다면, 이제... 답변을 하는 기능을 달아보도록 하겠습니다. ^^....
다들 아시다시피, 답변을 하기 위해서는 먼저, 어떤 글에 답변을 달 것인지를 결정해야 하겠지요? 해서, 답변을 위한 최적의 페이지는 Content.aspx가 될 것입니다. ^^;; 해서, 아시다시피 이미 만들어두었던 Content.aspx 페이지에는 [Reply]라는 버튼이 존재하고 있지요 ^^;
이전 강좌(계층형 게시판 첫 번째 강좌)에서 설명했듯이, 답변 글을 작성하기 위해서는, 자기 부모 글의 thread 와 depth 값이 필요합니다. 해서, Content.aspx 페이지에서 사용자가 [Reply] 버튼을 누를 경우에는 그러한 값들을 Insert.aspx 페이지로 넘겨주어야 합니다. 즉, 새 글쓰기를 위해서 뿐만 아니라 답변을 위해서도 Insert.aspx 페이지를 사용한다는 것인데요. Insert.aspx 페이지는 현재 구조상 새로운 글을 작성하는 것으로만 구성되어져 있습니다. 그러하기에, Insert.aspx 페이지도 기존의 로직을 변경해야 할 것입니다.
말이 좀 어려웠나요? 요즘 세상이 뒤숭숭해서 머리가 좀 정리가 안되어서 그런가 봅니다. 죄송합니다. ㅠㅠ. 반성하는 의미로, 다시금 간단하게 정리해 보도록 하겠습니다.
넹넹~~ 답변을 처리하기 위해서는 다음과 같은 절차를 통해서 기존 페이지를 변경하면 됩니다.
1. Content.aspx 페이지에서 [Reply] 버튼이 눌릴 경우, Insert.aspx 페이지로 현재 글의 thread 값과 depth 값을 넘겨주어야 합니다.
2. Insert.aspx 페이지에서는 Content.aspx 페이지에서 넘어온 thread, depth 값을 얻어서 만일 그 값들이 존재한다면, 현재 작성되는 글이 답변이라고 가정하고, 그러한 값들이 존재하지 않는다면 새글이라고 가정하여 각각 처리합니다. 해서, 입력 데이터를 답변글 또는 새 글로써 저장합니다. 물론, 답변 글을 저장하기 위해서는 별도의 프로시저가 필요할 것입니다. (이 프로시저는 이미 일전의 강좌에서 보여드린 적이 있습니다)
이해가 되시죠?
그렇다면, 하나씩 진행해 볼까요??? 먼저, Content.aspx 페이지를 변경해서 [Reply] 버튼이 눌릴 경우, 답변을 위해 필요한 정보인 thread, depth 값을 Insert.aspx 페이지로 넘기도록 만들어 보겠습니다. 이를 위해서는 VS.NET으로 프로젝트를 열어서, Content.aspx 웹 폼에서 [Reply] 버튼을 더블클릭하여, 코드 비하인드로 이동한 다음, 다음과 같은 코드를 작성하시면 될 것입니다. (하아하아~~ 너무 급하게 설명을????)
private void btnReply_Click(object sender, System.Web.UI.ImageClickEventArgs e) { string thread = ViewState["thread"].ToString(); string depth = ViewState["depth"].ToString();
Response.Redirect("insert.aspx?thread=" + thread + "&depth=" + depth); } |
대략~~
간단하죠??? 그렇습니다. 단지, ViewState 에 저장되어져 있는 현재 글의 thread 값과 depth 값을 얻어다가, 쿼리 문자열에 첨부해서 Insert.aspx 페이지로 URL을 이동하는 것이지요. 이렇게 하면, Insert.aspx 페이지에서는 Request.QueryString 이나 Request.Param 컬렉션을 통해서 이러한 값들을 얻어올 수 있을 것입니다. 이 방식은 기존 ASP에서 전형적으로 사용했는 방식이기도 합니다.
자. 코드를 작성하셨으면 이번에는 Insert.aspx 페이지로 넘어가 볼까요???
Insert.aspx 페이지에서는 먼저 이러한 값들이 제대로 넘어오는 지를 확인해 보기 위해서, 다음과 같은 임시 코드를 Page_Load 이벤트 처리기에 작성해 보도록 하겠습니다. ^^;
private string thread = string.Empty; private string depth = string.Empty;
private void Page_Load(object sender, System.EventArgs e) { if(!IsPostBack) { if (Request.Params["thread"] != null) thread = Request.Params["thread"].ToString(); if (Request.Params["depth"] != null) depth = Request.Params["depth"].ToString();
Response.Write("--Content.aspx 페이지에서 넘어온 값--<br>"); Response.Write("thread : " + thread + "<br>"); Response.Write("depth : " + depth + "<br>");
} |
자. 이렇게 코드를 작성한 다음에 Insert.aspx 페이지를 한번 확인해 보도록 할까요??? (임시 코드 자체는 여러분에게 그다지 어렵지 않으실 겁니다. 기존에 많이 다루어 보았던 코드일테니까요 ^^ 하하하)
일단, 글의 목록이 나타나는 경우에 [Write] 버튼을 눌러서 Insert.aspx 페이지로 이동하는 경우, 즉, 새글을 작성하는 경우의 Insert.aspx 페이지는 다음과 같은 모습을 나타낼 것입니다.

그쵸? 그쵸??? 그렇습니다. 이 경우는 새로운 글이기에, 넘어오는 thread 값과 depth 값이 없는 것을 보실 수 있을 것입니다. ^^;
그렇다면, 특정 글의 내용을 보는 페이지(Content.aspx)에서 [Reply] 버튼을 클릭하는 경우에는 어떻게 될까요? 하하하. 맞습니다~~~ 바로 다음과 같은 Insert.aspx 페이지를 마주하게 될 것입니다. 이 경우에는 thread 와 depth 값이 화면에 출력되어져 나온다는 것이죠~~

그렇습니다... 하하하.. (실없는 웃음이었습니다. ㅜㅜ)
자. 위의 코드를 통해서, 답변 글을 작성하려는 경우에만!!! Insert.aspx 페이지로 thread, depth 값들이 제대로 넘어온다는 사실을 확인할 수 있었습니다.
그렇다면, 이제 그에 따라서... 답변 글을 실제 데이터베이스에 저장하는 로직을 추가해야 하겠지요???
그렇다면, 기존의 새 글 쓰기를 위해서 준비되어져 있는 코드인 PostButton_Click 이벤트 처리기를 그렇게 수정하면 될 것입니다.(물론, 별도의 함수를 만들어서 사용해도 되겠지만, 저의 경우는 이렇게 처리하려 합니다) 즉, 이 이벤트 처리기를 수정해서, 현재의 글 쓰기가 '새 글쓰기'이면, 기존 처리 그대로 저장하게 하고, '답변 글 쓰기'이면 현재의 글을 답변으로써 저장하게 하면 된다는 것이지요.
먼저, 현재 Insert.aspx 페이지의 Page_Load 이벤트 처리기를 다음과 같이 작성해 보았습니다. ^^;
private bool ReplyFlag = false; private string thread = string.Empty; private string depth = string.Empty;
private void Page_Load(object sender, System.EventArgs e) { if (Request.Params["thread"] != null) thread = Request.Params["thread"].ToString(); if (Request.Params["depth"] != null) depth = Request.Params["depth"].ToString();
if(thread != string.Empty && depth != string.Empty) { ReplyFlag = true; } } |
thread 값을 저장할 thread 변수와 depth 값을 저장할 depth 변수는 클래스의 필드 변수로써 지정해 보았는데요. 그 이유는 이들을 클래스 내의 여러 메서드들에서 접근할 필요가 있기 때문에 그렇게 한 것입니다.
또한, ReplyFlag 이라는 불리언 변수도 하나 선언해 두었는데요. 이 또한 반드시 필요한 필드는 아닙니다만, 소스의 가독성을 높이려는 이유로 선언해 보았습니다. 원하신다면, 이 필드를 사용하지 않으셔도 무방합니다만... ^^;;; 그래도.. 가급적... 히히.. 부탁드립니다.
일단, 먼저 소스를 한번 주~~~욱 살펴볼까요?
먼저, Page_Load 이벤트 처리기에서는 이전 페이지에서 넘어왔을 지도 모르는 값인 thread 값과 depth 값을 Request.Param 컬렉션으로 가져와서 각각 thread, depth 필드변수에 저장하고 있습니다. 물론, 이 값은 '새글 쓰기'인 경우라면 빈 값일 것입니다. 이들은 오직 '답변글 쓰기'인 경우에만 값이 존재할테니까요 ^^
어쨋든, 이러한 처리로 인해 페이지가 처음 로드되는 경우이든, 포스트백 된 경우이든... thread와 depth라는 필드에는 부모글의 thread, depth 값이 들어있거나 혹은 빈 문자열이 들어있을 것임을 보장할 수 있을 것입니다.
그리고, 메서드의 마지막 부분에서는 thread와 depth 변수의 값이 모두 비어있지 않다면, ReplyFlag 를 true로 지정하고 있습니다. 즉, 그 경우에만 현재의 글이 답변 글이라고 표시하겠다는 것이지요. 해서, 이후의 코드에서는 단지 ReplyFlag 값이 true냐? false냐? 에 따라서 현재 글이 새글이냐? 답변글이냐?를 쉽게 파악할 수 있습니다. 이러한 목적으로 ReplyFlag 변수를 사용한 것입니다. 뭐~~ 이런 이유로 반드시 이 변수가 필요한 것은 아니지요. 하지만, 사용하면 가독성은 좀 나아질 것으로 사료됩니다만... ^^
자. 이제 기본적인 준비는 끝난 것 같네요. 그렇다면, 이제야 말로 실제로 입력된 글을 저장하는 로직만 남은 것 같습니다.
새글쓰기에 대한 저장 프로시저는 이미 전에 만들어 두었으니까요. 이번에 추가적으로 작성해야 하는 프로시저는 답변 글을 저장하기 위한 프로시저일 것입니다. 사실, 이 프로시저도 일전의 강좌(입력 프로시저 및 Insert.aspx의 작성 강좌 )에서 미리 보여드렸었죠?? 기억하시나요? 그렇습니다. 다음과 같았었습니다.
CREATE PROC dbo.UP_INSERT_BOARDREPLY @ParentThread int, @PrevThread int, @depth int, @Writer Varchar(20), @Pwd Varchar(20), @Email Varchar(100), @Title Varchar(100), @Mode Bit, @Ip Varchar(15), @Content Text AS SET NOCOUNT ON BEGIN TRAN
UPDATE ThreadBoard SET thread = thread - 1 Where thread < @ParentThread and thread > @PrevThread
IF (@@ERROR <> 0) BEGIN ROLLBACK TRAN RETURN END
INSERT INTO ThreadBoard (thread, depth, writer, pwd, email, title, mode, ip, content) Values (@ParentThread-1, @depth, @Writer, @Pwd, @Email, @Title, @Mode, @Ip, @Content)
IF (@@ERROR <> 0) BEGIN ROLLBACK TRAN RETURN END
COMMIT TRAN GO |
UP_INSERT_BOARDREPLY 라는 프로시저는 이미 게시판 로직 강좌에서 설명드렸던대로의 로직을 수행하고 있습니다. 혹시라도 기억이 가물가물하신 분은 다음 강좌를 다시금 읽어보시기 바랍니다.
바로 요 강좌!!! 클릭!!!!!!
한가지 기억해야 하는 것은... 방금 제작한 UP_INSERT_BOARDREPLY라는 프로시저와 기존에 존재하는 새글 쓰기용 프로시저인 UP_INSERT_BOARDNEW 와의 차이점 입니다. 내부적인 로직의 차이점은 그다지 중요하지 않습니다. ASP.NET 페이지에서 각각의 프로시저를 호출할 경우 중요한 부분은 그 프로시저의 내부로직이 아니라, 그 프로시저가 어떠한 인자들을 가지고 있느냐이니까요.
해서, 그 부분을 비교해 보면, UP_INSERT_BOARDREPLY 프로시저가 단지 UP_INSERT_BOARDNEW 프로시저보다 3개의 인자를 더 가지고 있다는 점이 차이점입니다. UP_INSERT_BOARDREPLY 프로시저의 인자를 살펴보면 앞쪽에 3개의 인자(@ParentThread, @PrevThread, @depth)가 더 추가되어져 있다는 것이지요. 그렇기에, ASP.NET 페이지에서 이 프로시저를 호출하려면 이러한 인자들도 당연히 추가적으로 지정해서 호출해 주어야 할 것입니다. 답변을 하기 위해서라면 말이지요 ^^;;
해서, 태오는 답변 글을 저장하기 위한 ASP.NET 쪽의 코드를 별도로 제작함 없이, 기존에 새글쓰기 처리를 위해서 준비되어져 있는 PostButton_Click 이벤트 처리기를 다소 변경하여 재사용할까 합니다. 프로시저의 인터페이스가 크게 차이가 없으니까 그래도 될 것 같아서요 ^^;
해서, 다음과 같이 PostButton_Click 코드를 변경해 보았습니다. 기존과 변경된 부분은 파란색으로 강조해 보았습니다.
private void PostButton_Click(object sender, System.Web.UI.ImageClickEventArgs e) {
Con = new SqlConnection(); Con.ConnectionString = "server=(local);database= TEST;userid=sa;password=";
string sp; if(ReplyFlag) sp = "UP_INSERT_BOARDREPLY"; else sp = "UP_INSERT_BOARDNEW";
Cmd = new SqlCommand(sp, Con); Cmd.CommandType = CommandType.StoredProcedure;
if(ReplyFlag) { Cmd.Parameters.Add("@ParentThread", SqlDbType.Int); Cmd.Parameters.Add("@PrevThread", SqlDbType.Int); Cmd.Parameters.Add("@depth", SqlDbType.Int); }
Cmd.Parameters.Add("@Writer", SqlDbType.VarChar, 20); Cmd.Parameters.Add("@Pwd", SqlDbType.VarChar, 20); Cmd.Parameters.Add("@Email", SqlDbType.VarChar, 100); Cmd.Parameters.Add("@Title", SqlDbType.VarChar, 100); Cmd.Parameters.Add("@Mode", SqlDbType.Bit); Cmd.Parameters.Add("@Ip", SqlDbType.VarChar, 15); Cmd.Parameters.Add("@Content", SqlDbType.Text);
if(ReplyFlag) { int thread = int.Parse(this.thread); int depth = int.Parse(this.depth); int prevThread = (thread-1)/1000 * 1000;
Cmd.Parameters["@ParentThread"].Value = thread; Cmd.Parameters["@PrevThread"].Value = prevThread; Cmd.Parameters["@depth"].Value = depth + 1; }
Cmd.Parameters["@Writer"].Value = name.Text; Cmd.Parameters["@Pwd"].Value = pwd.Text; Cmd.Parameters["@Email"].Value = mail.Text; Cmd.Parameters["@Title"].Value = title.Text; Cmd.Parameters["@Mode"].Value = (UseHTML.Checked == true ? 1 : 0); Cmd.Parameters["@Ip"].Value = Request.UserHostAddress; Cmd.Parameters["@Content"].Value = content.Text;
try { Con.Open(); Cmd.ExecuteNonQuery(); Con.Close();
Response.Redirect("list.aspx"); } catch(Exception ex) { lblError.Text = "ERROR : " + ex.Source + " - " + ex.Message; lblError.Visible = true; }
if(Con.State == ConnectionState.Open) Con.Close();
Cmd = null; Con = null; } |
파란색의 코드를 제외한다면, 그 외의 코드는 '새글 쓰기'를 위해 존재했던 코드와 동일합니다. 즉, 이 말은 현재 글 쓰기가 '답변글 쓰기'인 경우에만 파란색 구역을 추가적으로 기존 코드에 끼워넣겠다는 의미와 같은 것이지요. 그렇다면, 파란색 코드들을 하나씩 살펴볼까요???
첫번째 파란 코드 구역은 다음과 같습니다.
string sp;
if(ReplyFlag)
sp = "UP_INSERT_BOARDREPLY";
else
sp = "UP_INSERT_BOARDNEW";
Cmd = new SqlCommand(sp, Con);
그렇습니다. 이 코드는 ReplyFlag 값을 검사하여, 현재의 글이 새글이냐? 답변글이냐에 따라 sp라는 지역변수에 적절한 프로시저 명을 지정하는 코드입니다. 만일, 새글이라면 UP_INSERT_BOARDNEW라는 프로시저 문자열을 sp 변수에 지정하고, 답변 글이라면 UP_INSERT_BOARDREPLY이라는 문자열을 지정하는 것이지요. 해서, 그 프로시저를 사용하도록 하는 것입니다.
이제, 두번째 파란 구역의 코드를 살펴볼까요? 그 구역의 코드는 다음과 같습니다.
if(ReplyFlag)
{
Cmd.Parameters.Add("@ParentThread", SqlDbType.Int);
Cmd.Parameters.Add("@PrevThread", SqlDbType.Int);
Cmd.Parameters.Add("@depth", SqlDbType.Int);
}
이것은 답변 글 쓰기의 경우에 필요한 3개의 추가적인 파라미터들을 지정하는 코드입니다. 다시 한번, 강조하지만 이 코드는 오직 현재의 글쓰기가 답변글 쓰기일 경우에만 수행됩니다. 고로, 현재가 새 글쓰기인 경우라면 이 코드는 실행되지 않을 것이고, 따라서, 3개의 매개변수도 Command 개체에 추가되지 않을 것입니다. 어렵지 않죠???
그렇다면, 마지막이자 세번째인 구역의 코드를 살펴보도록 하겠습니다. 여기는 할 이야기가 조금 있을 것 같네요 ^^
if(ReplyFlag)
{
int thread = int.Parse(this.thread);
int depth = int.Parse(this.depth);
int prevThread = (thread-1)/1000 * 1000;
Cmd.Parameters["@ParentThread"].Value = thread;
Cmd.Parameters["@PrevThread"].Value = prevThread;
Cmd.Parameters["@depth"].Value = depth + 1;
}
이 부분은 두번째 파란구역에서 지정한 파라미터 개체들에게 값을 할당하는 코드입니다. 이 구역의 코드들 또한 오직 "답변 글 쓰기"인 경우에만 실행됩니다. 고로, 새 글 쓰기인 경우는 결코 이 코드가 실행되지 않을 것입니다.
값을 지정할 경우에 주의할 점은 @ParentThread 파라미터의 값은 부모 thread 값을 넣어야 하기에, 그냥 thread 변수 값을 사용하면 되지만, @PrevThread와 @depth 파라미터의 경우는 값을 지정하기 위해 추가적인 연산이 필요하다는 점입니다.
일단, depth의 경우는 그나마 간단합니다. 현재 답변 글은 이전 부모의 depth 보다 한 레벨이 높아야 하니깐.. 단지 int 형으로 형변환을 한 다음에, +1 을 해주기만 하면 되는데요...
PrevThread의 경우는 조금 까다롭지요. 이 값은 현재 글이 속해있는 최상위 부모들의 이전 글의 thread 값을 구해와야 하는 것이니까요. 이야기가 어렵다면 예를 들어보겠습니다.
즉, 현재 글의 thread가 3000 이라면, PrevThread는 2000 이 되어야 한다는 것이구요.
현재 글의 thread가 2994 인 경우에도 PrevThread는 2000이 되어야 한다는 것입니다. 마찬가지로,
현재 글의 thread가 4520 인 경우, PrevThread는 4000이 되어야 한다는 것이지요.
즉, 현재 글이 속해있는 원래 질문글의 바로 이전 질문글의 thread 값을 구해와야 한다는 것이지요. 이를 위해서는 거의 공식적인(?) 규칙이 하나 있는데요. 즉, (thread-1)/1000 * 1000 가 바로 그것입니다. 이 공식은 thread 값이 무엇이냐에 상관없이 우리가 원하는 답을 얻을 수 있게 해줍니다.
즉, thread 값이 2001~3000 사이에 있는 경우에는 PrevThread 값은 반드시 2000 이 나와야 한다는 것인데요. 위의 공식은 그러한 결과를 완벽하게 이끌어 내줍니다.
(thread-1)/1000 라는 부분이 소수점 이하 나머지는 버린다는 부분이 바로 핵심일 것입니다. 그렇기에... thread-1 값이 1000 보다 초과이고 2000 이하라면 언제나 그 값은 1이 나오게 되지요. 지금은 수학시간은 아니기에...(그리고, 저도 수학과 출신은 아니기에..) 왜 그렇게 되는지에 대한 수학적인 해석은 여러분의 몫으로 남겨두겠습니다. (사실, 저도 이젠 2차 방정식도 풀지 못하는 스똔이 되어버려서요..ㅜㅜ)
자. 어쨋든, 필요한 매개변수도 추가적으로 설정했고, 필요한 값들도 각각 구해서 넣어주었습니다. 이제 완성이 된 것이지요...
제가 말이 좀 많은 편이어서, 어쩌면 이번 강좌에서 설명한 내용이 오히려 복잡하고 헛갈리게 들리셨을지도 모르겠습니다. ㅠㅠ 그렇다면, 지금 즈음해서요... 강좌는 보지 마시고, 지금까지 작성된 코드만을 살펴보도록 하세요. 그렇다면, 생각보다 코드가 단순하고 처리해야 할 내용도 그리 많지 않다는 것을 깨달으실 수 있을 것입니다. 지나치게 자세하게 설명하려는 것이 제가 가진 병 중 하나인데요... 이 부분 양해해 주시구요~~ ^^;
자. 이제 코드가 완성되었으면 어디한번 실행해 볼까요???
우선, 게시판을 실행하고, 우선적으로 새로운 글을 하나 입력해 보시구요. 이어서, 그 글에 답변 글을 한번 작성해 보세요 ^^ 예상대로 멋지게 새로운 글과 답변 글이 달리는 것을 보실 수 있을 것입니다. 멋지죠??

하하하.... ^^
그렇습니다. 답변이 잘 달리는 것을 볼 수 있으시죠???
물론, 현재는 페이징 기능이 달려져 있지 않기에 목록은 그냥 주루룩~~~ 밑으로 끝없이 내려갈 것입니다만, 그 기능은 다음 강좌시간에 덧붙일 예정이니깐 지금 너무 걱정하지는 마세요 ^^
일단, 이번 강좌는 여기까지 구현하는 것으로써 마무리하려 합니다. 다음 강좌에서는 현재 존재하는 약간의 버그(?)들을 잡구요... 그리고, 현재의 게시판에 페이징 기능을 추가해 보려 합니다. ^^;;
게시판 강좌가 너무 오랜만에 올라와서 그런지... 조회수가 그리 높지 않네요... 아마도 많은 분들이 스스로 이 기능을 완성하셨기에 그러한 것이라 생각합니다... ㅜㅜ
그래도... 강좌는 마무리해야 하겠죠??? 조금 더 서둘러서 강좌를 마무리하고.. 계층형 게시판 2차 부분을 추진해 보도록 하겠습니다.
모두모두 좋은 하루 되세요~~~
참고!! 혹시 폼에서 태그나 스크립트를 입력한 뒤, Save를 하시면 다음과 같은 에러가 나나요? 
이것은 .NET 프레임워크 1.1.에서 추가된 기능때문에 그런데요. 태그나 스크립트 코드를 쿼리문자열로 넘길 경우, 서버측에서 보안적으로 위험해질 수 있는 상황을 미연에 방지하기 위해서, ASP.NET은 기본적으로 그러한 태그나 스크립트가 쿼리문자열로 넘어올 경우, 이러한 예외를 무조건적으로 발생시킵니다. 만일, 이러한 제약을 풀고 싶다면, 방법은 간단합니다. 이렇게 데이터를 넘기는 aspx 페이지의 @Page 지시문에 validateRequest 라는 어트리뷰트를 false라고 지정해 주시면 됩니다(이 정보는 현재의 에러 페이지를 자세히 보시면 영어로 자세히 써 있습니다. 영어로!!!!) <%@ Page language="c#" Codebehind="Insert.aspx.cs" AutoEventWireup="false" Inherits="AspNetTest.ThreadBoard.Insert" validateRequest="false" %> 이렇게 설정하시면 문제가 없을 것입니다. 여러분의 경우는 Insert.aspx 페이지와 Edit.aspx 페이지에 이러한 설정을 해주시면 될 것입니다. |