728x90
반응형
728x170
▶ TestDB.sql
CREATE TABLE dbo.Notice
(
ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY -- ID
,[Name] NVARCHAR(25) NOT NULL -- 작성자명
,MailAddress NVARCHAR(100) NULL -- 메일 주소
,Title NVARCHAR(150) NOT NULL -- 제목
,WriteDate DATETIME DEFAULT GETDATE() NOT NULL -- 작성일
,WriteIP NVARCHAR(15) NULL -- 작성 IP
,Content NTEXT NOT NULL -- 내용
,[Password] NVARCHAR(20) NULL -- 패스워드
,ReadCount INT DEFAULT 0 -- 조회 수
,[Encoding] NVARCHAR(10) NOT NULL -- 인코딩(HTML/TEXT)
,Homepage NVARCHAR(100) NULL -- 홈페이지
,UpdateDate DATETIME NULL -- 수정일
,UpdateIP NVARCHAR(15) NULL -- 수정 IP
,[FileName] NVARCHAR(255) NULL -- 파일명
,FileSize INT DEFAULT 0 -- 파일 크기
,DownloadCount INT DEFAULT 0 -- 다운로드 수
,ReferenceID INT NOT NULL -- 참조 ID
,ReplyLevel INT DEFAULT 0 -- 답변 레벨
,ReplyOrder INT DEFAULT 0 -- 답변 순서
,ReplyCount INT DEFAULT 0 -- 답변 수
,ParentID INT DEFAULT 0 -- 부모 ID
,CommentCount INT DEFAULT 0 -- 댓글 수
)
GO
CREATE TABLE dbo.NoticeComment
(
ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY -- ID
,NoticeID INT NOT NULL -- 게시판 ID
,[Name] NVARCHAR(25) NOT NULL -- 작성자명
,Comment NVARCHAR(4000) NOT NULL -- 내용
,WriteDate SMALLDATETIME DEFAULT(GETDATE()) -- 작성일
,[Password] NVARCHAR(20) NOT NULL -- 패스워드
)
GO
CREATE PROCEDURE dbo.WriteNotice
@Name NVARCHAR(25)
,@MailAddress NVARCHAR(100)
,@Title NVARCHAR(150)
,@WriteIP NVARCHAR(15)
,@Content NTEXT
,@Password NVARCHAR(20)
,@Encoding NVARCHAR(10)
,@Homepage NVARCHAR(100)
,@FileName NVARCHAR(255)
,@FileSize INT
AS
DECLARE @MaximumReferenceID INT;
SELECT @MaximumReferenceID = MAX(ReferenceID) FROM dbo.Notice;
IF @MaximumReferenceID IS NULL
BEGIN
SET @MaximumReferenceID = 1;
END
ELSE
BEGIN
SET @MaximumReferenceID = @MaximumReferenceID + 1;
END
INSERT INTO dbo.Notice
(
[Name]
,MailAddress
,Title
,WriteIP
,Content
,[Password]
,[Encoding]
,Homepage
,[ReferenceID]
,[FileName]
,[FileSize]
)
Values
(
@Name
,@MailAddress
,@Title
,@WriteIP
,@Content
,@Password
,@Encoding
,@Homepage
,@MaximumReferenceID
,@FileName
,@FileSize
);
GO
CREATE PROCEDURE dbo.ListNotice
@Page INT
AS
WITH OrderedNoticeBoard
AS
(
SELECT
ID
,[Name]
,MailAddress
,Title
,WriteDate
,ReadCount
,ReferenceID
,ReplyLevel
,ReplyOrder
,ReplyCount
,ParentID
,CommentCount
,[FileName]
,FileSize
,DownloadCount
,ROW_NUMBER() OVER (ORDER BY ReferenceID DESC, ReplyOrder ASC) AS 'RowNumber'
FROM dbo.Notice
)
SELECT *
FROM OrderedNoticeBoard
WHERE RowNumber BETWEEN @Page * 10 + 1 AND (@Page + 1) * 10;
GO
CREATE PROCEDURE dbo.ViewNotice
@ID Int
As
UPDATE dbo.Notice
SET ReadCount = ReadCount + 1
WHERE ID = @ID;
SELECT *
FROM dbo.Notice
WHERE ID = @ID;
GO
CREATE PROCEDURE dbo.ReplyNotice
@Name NVARCHAR(25)
,@MailAddress NVARCHAR(100)
,@Title NVARCHAR(150)
,@WriteIP NVARCHAR(15)
,@Content NTEXT
,@Password NVARCHAR(20)
,@Encoding NVARCHAR(10)
,@Homepage NVARCHAR(100)
,@ParentID INT
,@FileName NVARCHAR(255)
,@FileSize INT
AS
DECLARE @MaximumReplyOrder INT;
DECLARE @MaximumReplyCount INT;
DECLARE @ParentReferenceID INT;
DECLARE @ParentReplyLevel INT;
DECLARE @ParentReplyOrder INT;
UPDATE dbo.Notice
Set ReplyCount = ReplyCount + 1
WHERE ID = @ParentID;
SELECT
@MaximumReplyOrder = ReplyOrder
,@MaximumReplyCount = ReplyCount
FROM dbo.Notice
WHERE ParentID = @ParentID
AND ReplyOrder = (SELECT MAX(ReplyOrder) FROM dbo.Notice WHERE ParentID = @ParentID);
IF @MaximumReplyOrder IS NULL
BEGIN
SELECT @MaximumReplyOrder = ReplyOrder
FROM dbo.Notice
WHERE ID = @ParentID;
SET @MaximumReplyCount = 0;
END
SELECT
@ParentReferenceID = ReferenceID
,@ParentReplyLevel = ReplyLevel
FROM dbo.Notice
WHERE ID = @ParentID;
UPDATE dbo.Notice
SET ReplyOrder = ReplyOrder + 1
WHERE ReferenceID = @ParentReferenceID And ReplyOrder > (@MaximumReplyOrder + @MaximumReplyCount);
INSERT INTO dbo.Notice
(
[Name]
,MailAddress
,Title
,WriteIP
,Content
,[Password]
,[Encoding]
,Homepage
,ReferenceID
,ReplyLevel
,ReplyOrder
,ParentID
,[FileName]
,FileSize
)
VALUES
(
@Name
,@MailAddress
,@Title
,@WriteIP
,@Content
,@Password
,@Encoding
,@Homepage
,@ParentReferenceID
,@ParentReplyLevel + 1
,@MaximumReplyOrder + @MaximumReplyCount + 1
,@ParentID
,@FileName
,@FileSize
);
GO
CREATE PROCEDURE dbo.GetNoticeCount
As
Select Count(*) From dbo.Notice;
GO
CREATE PROCEDURE dbo.SearchNoticeCount
@SearchField NVARCHAR(25)
,@SearchQuery NVARCHAR(25)
AS
SET @SearchQuery = '%' + @SearchQuery + '%';
SELECT COUNT(*)
FROM dbo.Notice
WHERE
(
CASE @SearchField
WHEN 'Name' THEN [Name]
WHEN 'Title' THEN Title
WHEN 'Content' THEN Content
ELSE @SearchQuery
END
)
LIKE @SearchQuery;
GO
CREATE PROCEDURE dbo.DeleteNotice
@ID INT
,@Password NVARCHAR(30)
AS
DECLARE @Count INT;
SELECT @Count = COUNT(*)
FROM dbo.Notice
WHERE ID = @ID
AND [Password] = @Password;
IF @Count = 0
BEGIN
Return 0;
END
DECLARE @ReplyCount INT;
DECLARE @ReplyOrder INT;
DECLARE @ReferenceID INT;
DECLARE @ParentID INT;
SELECT
@ReplyCount = ReplyCount
,@ReplyOrder = ReplyOrder
,@ReferenceID = ReferenceID
,@ParentID = ParentID
FROM dbo.Notice
WHERE ID = @ID;
IF @ReplyCount = 0
BEGIN
IF @ReplyOrder > 0
BEGIN
UPDATE dbo.Notice
SET ReplyOrder = ReplyOrder - 1
WHERE ReferenceID = @ReferenceID
AND ReplyOrder > @ReplyOrder;
UPDATE Notice
SET ReplyCount = ReplyCount - 1
WHERE ID = @ParentID;
END
DELETE FROM dbo.Notice
WHERE ID = @ID;
DELETE FROM dbo.Notice
WHERE ID = @ParentID
AND UpdateIP = N'((DELETED))'
AND ReplyCount = 0;
END
ELSE
BEGIN
UPDATE dbo.Notice
SET
[Name] = N'(Unknown)',
MailAddress = '',
[Password] = '',
Title = N'(삭제된 글입니다.)',
Content = N'(삭제된 글입니다. 현재 답변이 포함되어 있기 때문에 내용만 삭제되었습니다.)',
UpdateIP = N'((DELETED))',
[FileName] = '',
FileSize = 0,
CommentCount = 0
WHERE ID = @ID;
END
GO
CREATE PROCEDURE dbo.UpdateNotice
@Name NVARCHAR(25)
,@MailAddress NVARCHAR(100)
,@Title NVARCHAR(150)
,@UpdateIP NVARCHAR(15)
,@Content NTEXT
,@Password NVARCHAR(30)
,@Encoding NVARCHAR(10)
,@Homepage NVARCHAR(100)
,@FileName NVARCHAR(255)
,@FileSize INT
,@ID INT
AS
DECLARE @Count INT
SELECT @Count = Count(*)
FROM dbo.Notice
WHERE ID = @ID
AND [Password] = @Password;
IF @Count > 0
BEGIN
UPDATE dbo.Notice
SET
[Name] = @Name
,MailAddress = @MailAddress
,Title = @Title
,UpdateIP = @UpdateIP
,UpdateDate = GETDATE()
,Content = @Content
,[Encoding] = @Encoding
,Homepage = @Homepage
,[FileName] = @FileName
,FileSize = @FileSize
Where ID = @ID;
SELECT '1';
END
ELSE
SELECT '0';
GO
CREATE PROCEDURE dbo.SearchNotice
@Page INT
,@SearchField NVARCHAR(25)
,@SearchQuery NVARCHAR(25)
AS
WITH OrderedNoticeBoard
AS
(
SELECT
ID
,[Name]
,MailAddress
,Title
,WriteDate
,ReadCount
,ReferenceID
,ReplyLevel
,ReplyOrder
,ReplyCount
,ParentID
,CommentCount
,[FileName]
,FileSize
,DownloadCount
,ROW_NUMBER() OVER (ORDER BY ReferenceID DESC, ReplyOrder ASC) AS 'RowNumber'
FROM dbo.Notice
WHERE
(
CASE @SearchField
WHEN 'Name' THEN [Name]
WHEN 'Title' THEN Title
WHEN 'Content' THEN Content
ELSE @SearchQuery
END
)
LIKE '%' + @SearchQuery + '%'
)
SELECT
ID
,[Name]
,MailAddress
,Title
,WriteDate
,ReadCount
,ReferenceID
,ReplyLevel
,ReplyOrder
,ReplyCount
,ParentID
,CommentCount
,[FileName]
,FileSize
,DownloadCount
,RowNumber
FROM OrderedNoticeBoard
WHERE RowNumber BETWEEN @Page * 10 + 1 AND (@Page + 1) * 10
ORDER BY ID DESC;
GO
728x90
▶ CommentControl.ascx
<%@ Control
Language="C#"
AutoEventWireup="true"
CodeBehind="CommentControl.ascx.cs"
Inherits="TestProject.NoticeBoard.Controls.CommentControl" %>
<asp:Repeater ID="repeater" runat="server">
<HeaderTemplate>
<table style="margin-left:20px;margin-right:20px;width:95%;padding:10px;">
</HeaderTemplate>
<ItemTemplate>
<tr style="border-bottom:1px dotted silver;">
<td style="width:80px;"><%# Eval("Name") %></td>
<td style="width:350px;"><%# TestProject.HTMLHelper.Encode(Eval("Comment").ToString()) %></td>
<td style="width:180px;"><%# Eval("WriteDate") %></td>
<td style="width:10px;text-align:center;">
<a title="댓글 삭제" href='CommentDeletePage.aspx?NoticeID=<%= Request["ID"] %>&ID=<%# Eval("ID") %>'>
<img border="0" src="/Image/NoticeBoard/delete.gif" />
</a>
</td>
</tr>
</ItemTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
</asp:Repeater>
<table style="margin-left:auto;width:500px;">
<tr>
<td style="width:64px;text-align:right;">성명 </td>
<td style="width:128px;">
<asp:TextBox ID="nameTextBox" runat="server"
CssClass="form-control"
Style="display:inline-block;"
Width="128px" />
</td>
<td style="width:64px;text-align:right;">패스워드 </td>
<td style="width:128px;">
<asp:TextBox ID="passwordTextBox" runat="server"
CssClass="form-control"
Style="display:inline-block;"
Width="128px"
TextMode="Password" />
</td>
<td style="width:128px;text-align:right;">
<asp:Button ID="writeButton" runat="server"
CssClass="form-control btn btn-primary"
Style="display: inline-block;"
Width="96px"
Text="의견 남기기"
OnClick="writeButton_Click" />
</td>
</tr>
<tr>
<td style="width:64px;text-align:right;">댓글 </td>
<td style="width:448px;" colspan="4">
<asp:TextBox ID="commentTextBox" runat="server"
CssClass="form-control"
Style="display:inline-block;"
Width="448px"
Rows="3"
Columns="70"
TextMode="MultiLine" />
</td>
</tr>
</table>
<hr />
300x250
▶ CommentControl.ascx.cs
using System;
using System.Web.UI;
using NoticeBoard.Models;
namespace TestProject.NoticeBoard.Controls
{
/// <summary>
/// 댓글 컨트롤
/// </summary>
public partial class CommentControl : UserControl
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Field
////////////////////////////////////////////////////////////////////////////////////////// Private
#region Field
/// <summary>
/// 댓글 저장소
/// </summary>
private CommentRepository repository;
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
////////////////////////////////////////////////////////////////////////////////////////// Public
#region 생성자 - CommentControl()
/// <summary>
/// 생성자
/// </summary>
public CommentControl()
{
this.repository = new CommentRepository();
}
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
if(!Page.IsPostBack)
{
this.repeater.DataSource = this.repository.GetCommentList(Convert.ToInt32(Request["ID"]));
this.repeater.DataBind();
}
}
#endregion
#region 의견 남기기 버튼 클릭시 처리하기 - writeButton_Click(sender, e)
/// <summary>
/// 의견 남기기 버튼 클릭시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void writeButton_Click(object sender, EventArgs e)
{
CommentModel comment = new CommentModel();
comment.NoticeID = Convert.ToInt32(Request["ID"]);
comment.Name = this.nameTextBox.Text;
comment.Password = this.passwordTextBox.Text;
comment.Comment = this.commentTextBox.Text;
this.repository.AddComment(comment);
Response.Redirect($"{Request.ServerVariables["SCRIPT_NAME"]}?ID={Request["ID"]}");
}
#endregion
}
}
▶ EditorControl.ascx
<%@ Control
Language="C#"
AutoEventWireup="true"
CodeBehind="EditorControl.ascx.cs"
Inherits="TestProject.NoticeBoard.Controls.EditorControl" %>
<style>
.BoardWriteFormTableLeftStyle
{
width : 100px;
text-align : right;
}
</style>
<h2 style="text-align:center;">게시판</h2>
<asp:Label ID="titleDescriptionLabel" runat="server"
ForeColor="#ff0000" />
<hr />
<table
style="margin-left:auto;margin-right:auto;width:600px;border-collapse:collapse;padding:5px;">
<% if(!string.IsNullOrEmpty(Request.QueryString["ID"]) && FormType == NoticeBoard.Models.BoardWriteFormType.Modify) { %>
<tr>
<td class="BoardWriteFormTableLeftStyle">
<span style="color: #ff0000;">*</span>ID
</td>
<td style="width:500px;">
<%= Request.QueryString["ID"] %>
</td>
</tr>
<% } %>
<tr>
<td class="BoardWriteFormTableLeftStyle"><span style="color: #ff0000;">*</span>성명</td>
<td style="width:500px;">
<asp:TextBox ID="nameTextBox" runat="server"
CssClass="form-control"
MaxLength="10"
Width="150px" />
<asp:RequiredFieldValidator ID="nameRequiredFieldValidator" runat="server"
ControlToValidate="nameTextBox"
Display="None"
SetFocusOnError="True"
ErrorMessage="* 성명을 입력해 주시기 바랍니다." />
</td>
</tr>
<tr>
<td style="text-align:right;">메일 주소</td>
<td>
<asp:TextBox ID="mailAddressTextBox" runat="server"
CssClass="form-control"
style="display:inline-block;"
MaxLength="80"
Width="200px" />
<span style="color:#aaaaaa;font-style:italic">(선택 사항)</span>
<asp:RegularExpressionValidator ID="mailAddressRegularExpressionValidator" runat="server"
ControlToValidate="mailAddressTextBox"
ValidationExpression="\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*"
Display="None"
SetFocusOnError="True"
ErrorMessage="* 메일 주소를 입력해 주시기 바랍니다." />
</td>
</tr>
<tr>
<td style="text-align:right;">홈페이지</td>
<td>
<asp:TextBox ID="homepageTextBox" runat="server"
CssClass="form-control"
style="display:inline-block;"
MaxLength="80"
Width="300px" />
<span style="color:#aaaaaa;font-style:italic;">(선택 사항)</span>
<asp:RegularExpressionValidator ID="homepageRegularExpressionValidator" runat="server"
ControlToValidate="homepageTextBox"
ValidationExpression="http://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?"
Display="None"
SetFocusOnError="True"
ErrorMessage="* 홈페이지를 입력해 주시기 바랍니다." />
</td>
</tr>
<tr>
<td style="text-align:right;"><span style="color: #ff0000;">*</span>제목</td>
<td>
<asp:TextBox ID="titleTextBox" runat="server" CssClass="form-control"
Width="480px"></asp:TextBox>
<asp:RequiredFieldValidator ID="titleRequiredFieldValidator" runat="server"
ControlToValidate="titleTextBox"
Display="None"
SetFocusOnError="True"
ErrorMessage="* 제목을 입력해 주시기 바랍니다." />
</td>
</tr>
<tr>
<td style="text-align:right;"><span style="color: #ff0000;">*</span>내용</td>
<td>
<asp:TextBox ID="contentTextBox" runat="server"
CssClass="form-control"
style="display:inline-block;"
Width="480px"
Height="150px"
TextMode="MultiLine" />
<asp:RequiredFieldValidator ID="contentRequiredFieldValidator" runat="server"
ControlToValidate="contentTextBox"
Display="None"
SetFocusOnError="True"
ErrorMessage="* 내용을 입력해 주시기 바랍니다." />
</td>
</tr>
<tr>
<td style="text-align:right;">파일 첨부</td>
<td>
<asp:CheckBox ID="uploadCheckBox" runat="server"
CssClass="check-inline"
AutoPostBack="True"
Text="체크 박스를 체크하면 업로드 화면이 표시됩니다."
OnCheckedChanged="uploadCheckBox_CheckedChanged" />
<span style="color:#aaaaaa;font-style:italic">(선택 사항)</span>
<br />
<asp:Panel ID="filePanel" runat="server"
Width="480px"
Height="25px"
Visible="false">
<input id="fileNameButton" runat="server"
style="width:290px;height:22px"
name="fileNameTextBox"
type="file" />
<asp:label ID="previousFileNameLabel" runat="server"
text=""
Visible="false" />
</asp:Panel>
</td>
</tr>
<tr>
<td style="text-align:right;"><span style="color: #ff0000;">*</span>인코딩</td>
<td>
<asp:RadioButtonList ID="encodingRadioButtonList" runat="server"
RepeatDirection="Horizontal"
RepeatLayout="Flow">
<asp:ListItem Value="Text" Selected="True">텍스트</asp:ListItem>
<asp:ListItem Value="HTML">HTML</asp:ListItem>
<asp:ListItem Value="Mixed">혼합</asp:ListItem>
</asp:RadioButtonList>
</td>
</tr>
<tr>
<td style="text-align:right;"><span style="color: #ff0000;">*</span>패스워드</td>
<td>
<asp:TextBox ID="passwordTextBox" runat="server"
CssClass="form-control"
style="display:inline-block;"
MaxLength="20"
Width="150px"
TextMode="Password"
EnableViewState="False" />
<span style=" color: #aaaaaa;">(수정/삭제시 필요)</span>
<asp:RequiredFieldValidator ID="passwordRequiredFieldValidator" runat="server"
ControlToValidate="passwordTextBox"
Display="None"
SetFocusOnError="True"
ErrorMessage="* 패스워드를 입력해 주시기 바랍니다." />
</td>
</tr>
<% if(!Page.User.Identity.IsAuthenticated) { %>
<tr>
<td style="text-align:right;"><span style="color: #ff0000;">*</span>보안코드</td>
<td>
<asp:TextBox ID="imageTextTextBox" runat="server"
CssClass="form-control"
style="display:inline-block;"
MaxLength="20"
Width="150px"
EnableViewState="False" />
<span style=" color: #aaaaaa;">(아래에 제시되는 보안코드를 입력하십시오.)</span>
<br />
<asp:Image ID="imageTextImage" runat="server"
ImageUrl="~/NoticeBoard/ImageTextPage.aspx" />
<asp:Label ID="errorLabel" runat="server"
ForeColor="Red" />
</td>
</tr>
<% } %>
<tr>
<td colspan="2" style="text-align:center;">
<asp:Button ID="saveButton" runat="server"
CssClass="btn btn-primary"
Text="저장"
OnClick="saveButton_Click" />
<a class="btn btn-default" href="NoticeListPage.aspx">목록</a>
<br />
<asp:ValidationSummary ID="validationSummary" runat="server"
ShowSummary="False"
ShowMessageBox="True"
DisplayMode="List" />
<br />
</td>
</tr>
</table>
▶ EditorControl.ascx.cs
using System;
using System.IO;
using System.Web.UI;
using NoticeBoard.Models;
namespace TestProject.NoticeBoard.Controls
{
/// <summary>
/// 에디터 컨트롤
/// </summary>
public partial class EditorControl : UserControl
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Field
////////////////////////////////////////////////////////////////////////////////////////// Private
#region Field
/// <summary>
/// ID
/// </summary>
private string id;
/// <summary>
/// 업로드 디렉토리 경로
/// </summary>
private string uploadDirectoryPath = string.Empty;
/// <summary>
/// 파일명
/// </summary>
private string fileName = string.Empty;
/// <summary>
/// 파일 크기
/// </summary>
private int fileSize = 0;
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Property
////////////////////////////////////////////////////////////////////////////////////////// Public
#region 폼 타입 - FormType
/// <summary>
/// 폼 타입
/// </summary>
public BoardWriteFormType FormType { get; set; }
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
this.id = Request.QueryString["ID"];
if(!Page.IsPostBack)
{
switch(FormType)
{
case BoardWriteFormType.Write :
this.titleDescriptionLabel.Text = "글 쓰기 - 아래 항목을 입력해 주시기 바랍니다.";
break;
case BoardWriteFormType.Modify :
this.titleDescriptionLabel.Text = "글 수정 - 아래 항목을 입력해 주시기 바랍니다.";
DisplayDataForUpdate();
break;
case BoardWriteFormType.Reply :
this.titleDescriptionLabel.Text = "글 답변 - 아래 항목을 입력해 주시기 바랍니다.";
DisplayDataForReply();
break;
}
}
}
#endregion
#region 업로드 체크 박스 체크 변경시 처리하기 - uploadCheckBox_CheckedChanged(sender, e)
/// <summary>
/// 업로드 체크 박스 체크 변경시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void uploadCheckBox_CheckedChanged(object sender, EventArgs e)
{
this.filePanel.Visible = !this.filePanel.Visible;
}
#endregion
#region 저장 버튼 클릭시 처리하기 - saveButton_Click(object sender, EventArgs e)
/// <summary>
/// 저장 버튼 클릭시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void saveButton_Click(object sender, EventArgs e)
{
if(ValidateSecurtyImageText())
{
UploadFile();
NoticeModel notice = new NoticeModel();
notice.ID = Convert.ToInt32(this.id);
notice.Name = this.nameTextBox.Text;
notice.MailAddress = HTMLHelper.Encode(this.mailAddressTextBox.Text);
notice.Homepage = this.homepageTextBox.Text;
notice.Title = HTMLHelper.Encode(this.titleTextBox.Text);
notice.Content = this.contentTextBox.Text;
notice.FileName = this.fileName;
notice.FileSize = this.fileSize;
notice.Password = this.passwordTextBox.Text;
notice.WriteIP = Request.UserHostAddress;
notice.Encoding = this.encodingRadioButtonList.SelectedValue;
NoticeRepository repository = new NoticeRepository();
switch(FormType)
{
case BoardWriteFormType.Write :
repository.Add(notice);
Response.Redirect("NoticeListPage.aspx");
break;
case BoardWriteFormType.Modify :
notice.UpdateIP = Request.UserHostAddress;
notice.FileName = ViewState["FileName"].ToString();
notice.FileSize = Convert.ToInt32(ViewState["FileSize"]);
int recordCount = repository.Update(notice);
if(recordCount > 0)
{
Response.Redirect($"NoticeViewPage.aspx?ID={this.id}");
}
else
{
this.errorLabel.Text = "수정을 실패했습니다. 암호를 확인해 주시기 바랍니다.";
}
break;
case BoardWriteFormType.Reply :
notice.ParentID = Convert.ToInt32(this.id);
repository.Reply(notice);
Response.Redirect("NoticeListPage.aspx");
break;
default :
repository.Add(notice);
Response.Redirect("NoticeListPage.aspx");
break;
}
}
else
{
this.errorLabel.Text = "보안코드가 일치하지 않습니다.. 다시 입력해 주시기 바랍니다.";
}
}
#endregion
////////////////////////////////////////////////////////////////////////////////////////// Private
#region 수정용 데이터 표시하기 - DisplayDataForUpdate()
/// <summary>
/// 수정용 데이터 표시하기
/// </summary>
private void DisplayDataForUpdate()
{
NoticeModel notice = (new NoticeRepository()).Get(Convert.ToInt32(this.id));
this.nameTextBox.Text = notice.Name;
this.mailAddressTextBox.Text = notice.MailAddress;
this.homepageTextBox.Text = notice.Homepage;
this.titleTextBox.Text = notice.Title;
this.contentTextBox.Text = notice.Content;
string ecoding = notice.Encoding;
if(ecoding == "Text")
{
this.encodingRadioButtonList.SelectedIndex = 0;
}
else if(ecoding == "Mixed")
{
this.encodingRadioButtonList.SelectedIndex = 2;
}
else
{
this.encodingRadioButtonList.SelectedIndex = 1;
}
if(notice.FileName.Length > 1)
{
ViewState["FileName"] = notice.FileName;
ViewState["FileSize"] = notice.FileSize;
this.filePanel.Height = 50;
this.previousFileNameLabel.Visible = true;
this.previousFileNameLabel.Text = $"기존 업로드 파일명 : {notice.FileName}";
}
else
{
ViewState["FileName"] = "";
ViewState["FileSize"] = 0;
}
}
#endregion
#region 답변용 데이터 표시하기 - DisplayDataForReply()
/// <summary>
/// 답변용 데이터 표시하기
/// </summary>
private void DisplayDataForReply()
{
NoticeModel notice = (new NoticeRepository()).Get(Convert.ToInt32(this.id));
this.titleTextBox.Text = $"Re : {notice.Title}";
this.contentTextBox.Text = $"\n\nOn {notice.WriteDate}, '{notice.Name}' wrote:\n----------\n>{notice.Content.Replace("\n", "\n>")}\n---------";
}
#endregion
#region 보안 이미지 텍스트 검증하기 - ValidateSecurtyImageText()
/// <summary>
/// 보안 이미지 텍스트 검증하기
/// </summary>
private bool ValidateSecurtyImageText()
{
if(Page.User.Identity.IsAuthenticated)
{
return true;
}
else
{
if(Session["ImageText"] != null)
{
return (this.imageTextTextBox.Text == Session["ImageText"].ToString());
}
}
return false;
}
#endregion
#region 파일 업로드하기 - UploadFile()
/// <summary>
/// 파일 업로드하기
/// </summary>
private void UploadFile()
{
this.uploadDirectoryPath = Server.MapPath("~/FileUpload");
this.fileName = string.Empty;
this.fileSize = 0;
if(this.fileNameButton.PostedFile != null)
{
if(this.fileNameButton.PostedFile.FileName.Trim().Length > 0 && this.fileNameButton.PostedFile.ContentLength > 0)
{
if(FormType == BoardWriteFormType.Modify)
{
ViewState["FileName"] = FileHelper.GetUniqueFileName
(
this.uploadDirectoryPath,
Path.GetFileName(this.fileNameButton.PostedFile.FileName)
);
ViewState["FileSize"] = this.fileNameButton.PostedFile.ContentLength;
this.fileNameButton.PostedFile.SaveAs
(
Path.Combine
(
this.uploadDirectoryPath,
ViewState["FileName"].ToString()
)
);
}
else
{
this.fileName = FileHelper.GetUniqueFileName
(
this.uploadDirectoryPath,
Path.GetFileName(this.fileNameButton.PostedFile.FileName)
);
this.fileSize = this.fileNameButton.PostedFile.ContentLength;
this.fileNameButton.PostedFile.SaveAs(Path.Combine(this.uploadDirectoryPath, this.fileName));
}
}
}
}
#endregion
}
}
▶ PagingControl.ascx
<%@ Control
Language="C#"
AutoEventWireup="true"
CodeBehind="PagingControl.ascx.cs"
Inherits="TestProject.NoticeBoard.Controls.PagingControl" %>
<asp:Literal ID="pagingLiteral" runat="server" />
▶ PagingControl.ascx.cs
using System;
using System.ComponentModel;
using System.Web.UI;
namespace TestProject.NoticeBoard.Controls
{
/// <summary>
/// 페이징 컨트롤
/// </summary>
public partial class PagingControl : UserControl
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Field
////////////////////////////////////////////////////////////////////////////////////////// Private
#region Field
/// <summary>
/// 레코드 수
/// </summary>
private int recordCount;
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Property
////////////////////////////////////////////////////////////////////////////////////////// Public
#region 검색 모드 - SearchMode
/// <summary>
/// 검색 모드
/// </summary>
public bool SearchMode { get; set; } = false;
#endregion
#region 검색 필드 - SearchField
/// <summary>
/// 검색 필드
/// </summary>
public string SearchField { get; set; }
#endregion
#region 검색 쿼리 - SearchQuery
/// <summary>
/// 검색 쿼리
/// </summary>
public string SearchQuery { get; set; }
#endregion
#region 페이지 인덱스 - PageIndex
/// <summary>
/// 페이지 인덱스
/// </summary>
[Category("PagingControl")]
public int PageIndex { get; set; }
#endregion
#region 페이지 수 - PageCount
/// <summary>
/// 페이지 수
/// </summary>
[Category("PagingControl")]
public int PageCount { get; set; }
#endregion
#region 페이지 크기 - PageSize
/// <summary>
/// 페이지 크기
/// </summary>
[Category("PagingControl")]
public int PageSize { get; set; } = 10;
#endregion
#region 레코드 수 - RecordCount
/// <summary>
/// 레코드 수
/// </summary>
[Category("PagingControl")]
public int RecordCount
{
get
{
return this.recordCount;
}
set
{
this.recordCount = value;
PageCount = ((this.recordCount - 1) / PageSize) + 1;
}
}
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
SearchMode = (!string.IsNullOrEmpty(Request.QueryString["SearchField"]) && !string.IsNullOrEmpty(Request.QueryString["SearchQuery"]));
if(SearchMode)
{
SearchField = Request.QueryString["SearchField"];
SearchQuery = Request.QueryString["SearchQuery"];
}
PageIndex++;
int i;
string html = "<ul class='pagination pagination-sm'>";
if(PageIndex > 10)
{
if(SearchMode)
{
html += "<li><a href=\"" +
Request.ServerVariables["SCRIPT_NAME"] +
"?Page=" +
Convert.ToString(((PageIndex - 1) / (int)10) * 10) +
"&SearchField=" +
SearchField +
"&SearchQuery=" +
SearchQuery +
"\">◀</a></li>";
}
else
{
html += "<li><a href=\"" +
Request.ServerVariables["SCRIPT_NAME"] +
"?Page=" +
Convert.ToString(((PageIndex - 1) / (int)10) * 10) +
"\">◀</a></li>";
}
}
else
{
html += "<li class=\"disabled\"><a>◁</a></li>";
}
for(i = (((PageIndex - 1) / (int)10) * 10 + 1); i <= ((((PageIndex - 1) / (int)10) + 1) * 10); i++)
{
if(i > PageCount)
{
break;
}
if(i == PageIndex)
{
html += " <li class='active'><a href='#'>" + i.ToString() + "</a></li>";
}
else
{
if(SearchMode)
{
html += "<li><a href=\"" +
Request.ServerVariables["SCRIPT_NAME"] +
"?Page=" +
i.ToString() +
"&SearchField=" +
SearchField +
"&SearchQuery=" +
SearchQuery +
"\">" +
i.ToString() +
"</a></li>";
}
else
{
html += "<li><a href=\"" +
Request.ServerVariables["SCRIPT_NAME"] +
"?Page=" +
i.ToString() +
"\">" +
i.ToString() +
"</a></li>";
}
}
}
if(i < PageCount)
{
if(SearchMode)
{
html += "<li><a href=\"" +
Request.ServerVariables["SCRIPT_NAME"] +
"?Page=" +
Convert.ToString(((PageIndex - 1) / (int)10) * 10 + 11) +
"&SearchField=" +
SearchField +
"&SearchQuery=" +
SearchQuery +
"\">▶</a></li>";
}
else
{
html += "<li><a href=\"" +
Request.ServerVariables["SCRIPT_NAME"] +
"?Page=" +
Convert.ToString(((PageIndex - 1) / (int)10) * 10 + 11) +
"\">▶</a></li>";
}
}
else
{
html += "<li class=\"disabled\"><a>▷</a></li>";
}
html += "</ul>";
this.pagingLiteral.Text = html;
}
#endregion
}
}
▶ SearchControl.ascx
<%@ Control
Language="C#"
AutoEventWireup="true"
CodeBehind="SearchControl.ascx.cs"
Inherits="TestProject.NoticeBoard.Controls.SearchControl" %>
<div style="text-align:center;">
<asp:DropDownList ID="searchFieldDropDownList" runat="server"
CssClass="form-control"
Style="display:inline-block;"
Width="80px">
<asp:ListItem Value="Name">성명</asp:ListItem>
<asp:ListItem Value="Title">제목</asp:ListItem>
<asp:ListItem Value="Content">내용</asp:ListItem>
</asp:DropDownList>
<asp:TextBox ID="searchQueryTextBox" runat="server"
CssClass="form-control"
Style="display: inline-block;"
Width="200px" />
<asp:RequiredFieldValidator ID="searchQueryRequiredFieldValidator" runat="server"
ControlToValidate="searchQueryTextBox"
Display="None"
ErrorMessage="검색할 단어를 입력하세요." />
<asp:ValidationSummary ID="validationSummary" runat="server"
ShowSummary="False"
ShowMessageBox="True" />
<asp:Button ID="searchButton" runat="server"
CssClass="form-control"
Style="display:inline-block;"
Width="100px"
Text="검색"
OnClick="searchButton_Click" />
</div>
<br />
<% if(!string.IsNullOrEmpty(Request.QueryString["SearchField"]) && !String.IsNullOrEmpty(Request.QueryString["SearchQuery"])) { %>
<div style="text-align:center;">
<a class="btn btn-success" href="/NoticeBoard/NoticeListPage.aspx">검색 완료</a>
</div>
<% } %>
▶ SearchControl.ascx.cs
using System;
using System.Web.UI;
namespace TestProject.NoticeBoard.Controls
{
/// <summary>
/// 검색 컨트롤
/// </summary>
public partial class SearchControl : UserControl
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
}
#endregion
#region 검색 버튼 클릭시 처리하기 - searchButton_Click(sender, e)
/// <summary>
/// 검색 버튼 클릭시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void searchButton_Click(object sender, EventArgs e)
{
string url = string.Format
(
"/NoticeBoard/NoticeListPage.aspx?SearchField={0}&SearchQuery={1}",
this.searchFieldDropDownList.SelectedItem.Value,
this.searchQueryTextBox.Text
);
Response.Redirect(url);
}
#endregion
}
}
▶ CommentDeletePage.aspx
<%@ Page
Language="C#"
MasterPageFile="~/Site.Master"
AutoEventWireup="true"
CodeBehind="CommentDeletePage.aspx.cs"
Inherits="TestProject.NoticeBoard.CommentDeletePage"
Title="댓글 삭제 페이지" %>
<asp:Content ID="content" ContentPlaceHolderID="MainContent" runat="server">
<h2 style="text-align: center;">게시판</h2>
<span style="color: #ff0000">댓글 삭제 - 패스워드를 입력하면 댓글을 삭제할 수 있습니다.</span>
<hr />
<table style="width:500px;margin-left:auto;margin-right:auto;">
<tr>
<td colspan="2">
<i class="glyphicon glyphicon-lock"></i>
<span style="font-size:12pt;">댓글 삭제</span>
</td>
</tr>
<tr>
<td> </td>
<td>
<span>해당 댓글을 삭제하시려면 패스워드를 입력해 주시기 바랍니다.</span>
<br />
패스워드(<u>P</u>):
<asp:TextBox ID="passwordTextBox" runat="server"
CssClass="form-control"
Style="display:inline-block;"
MaxLength="40"
Width="250px"
TextMode="Password"
AccessKey="P"
TabIndex="2" />
</td>
</tr>
<tr>
<td colspan="2" style="text-align:center;">
<asp:Button ID="deleteButton" runat="server"
CssClass="btn btn-danger"
Text="삭제"
OnClick="deleteButton_Click" />
<asp:RequiredFieldValidator ID="passwordRequiredFieldValidator" runat="server"
ControlToValidate="passwordTextBox"
Display="None"
ErrorMessage="패스워드를 입력해 주시기 바랍니다." />
<asp:ValidationSummary ID="validationSummary" runat="server"
ShowMessageBox="true"
ShowSummary="false" />
<input type="button"
class="btn btn-default"
value="이전으로"
onclick="history.go(-1);" />
<br />
<asp:Label ID="errorLabel" runat="server"
ForeColor="Red" />
</td>
</tr>
</table>
</asp:Content>
▶ CommentDeletePage.aspx.cs
using System;
using System.Web.UI;
using NoticeBoard.Models;
namespace TestProject.NoticeBoard
{
/// <summary>
/// 댓글 삭제 페이지
/// </summary>
public partial class CommentDeletePage : Page
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Property
////////////////////////////////////////////////////////////////////////////////////////// Public
#region 게시글 ID - NoticeID
/// <summary>
/// 게시글 ID
/// </summary>
public int NoticeID { get; set; }
#endregion
#region 댓글 ID - ID
/// <summary>
/// 댓글 ID
/// </summary>
public int ID { get; set; }
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
if(Request["NoticeID"] != null && Request.QueryString["ID"] != null)
{
NoticeID = Convert.ToInt32(Request["NoticeID"]);
ID = Convert.ToInt32(Request["ID" ]);
}
else
{
Response.End();
}
}
#endregion
#region 삭제 버튼 클릭시 처리하기 - deleteButton_Click(sender, e)
/// <summary>
/// 삭제 버튼 클릭시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void deleteButton_Click(object sender, EventArgs e)
{
CommentRepository repository = new CommentRepository();
if(repository.GetCommentCount(NoticeID, ID, this.passwordTextBox.Text) > 0)
{
repository.DeleteComment(NoticeID, ID, this.passwordTextBox.Text);
Response.Redirect($"NoticeViewPage.aspx?ID={NoticeID}");
}
else
{
this.errorLabel.Text = "패스워드가 일치하지 않습니다. 다시 입력해 주시기 바랍니다.";
}
}
#endregion
}
}
▶ ImageDownloadPage.aspx
<%@ Page
Language="C#"
AutoEventWireup="true"
CodeBehind="ImageDownloadPage.aspx.cs"
Inherits="TestProject.NoticeBoard.ImageDownloadPage" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
</head>
<body>
<form id="form" runat="server">
<div>
</div>
</form>
</body>
</html>
▶ ImageDownloadPage.aspx.cs
using System;
using System.IO;
using System.Web.UI;
namespace TestProject.NoticeBoard
{
/// <summary>
/// 이미지 다운로드 페이지
/// </summary>
public partial class ImageDownloadPage : Page
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
if(string.IsNullOrEmpty(Request.QueryString["FileName"]))
{
Response.End();
}
string fileName = Request.Params["FileName"].ToString();
string fileExtension = Path.GetExtension(fileName);
string contentType = string.Empty;
if(fileExtension == ".gif" || fileExtension == ".jpg" || fileExtension == ".jpeg" || fileExtension == ".png")
{
switch(fileExtension)
{
case ".gif" : contentType = "image/gif"; break;
case ".jpg" : contentType = "image/jpeg"; break;
case ".jpeg" : contentType = "image/jpeg"; break;
case ".png" : contentType = "image/png"; break;
}
}
else
{
Response.Write("<script language='javascript'>alert('이미지 파일이 아닙니다.');</script>");
Response.End();
}
string filePath = Server.MapPath("~/FileUpload/") + fileName;
Response.Clear();
Response.ContentType = contentType;
Response.WriteFile(filePath);
Response.End();
}
#endregion
}
}
▶ ImageTextPage.aspx
<%@ Page
Language="C#"
AutoEventWireup="true"
CodeBehind="ImageTextPage.aspx.cs"
Inherits="TestProject.NoticeBoard.ImageTextPage" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
</div>
</form>
</body>
</html>
▶ ImageTextPage.aspx.cs
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.Web.UI;
namespace TestProject.NoticeBoard
{
/// <summary>
/// 이미지 텍스트 페이지
/// </summary>
public partial class ImageTextPage : Page
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
Bitmap bitmap = new Bitmap(80, 20);
Graphics graphics = Graphics.FromImage(bitmap);
graphics.Clear(Color.White);
graphics.SmoothingMode = SmoothingMode.HighSpeed;
graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
Random random = new Random();
char character1 = (char)random.Next(65, 90 );
char character2 = (char)random.Next(48, 57 );
char character3 = (char)random.Next(97, 122);
char character4 = (char)random.Next(48, 57 );
Session["ImageText"] = $"{character1}{character2}{character3}{character4}";
graphics.DrawString(character1.ToString(), new Font("Verdana", 12, FontStyle.Bold ), Brushes.DarkBlue, new PointF(5 , 1));
graphics.DrawString(character2.ToString(), new Font("Arial" , 11, FontStyle.Italic ), Brushes.DarkBlue, new PointF(25, 1));
graphics.DrawString(character3.ToString(), new Font("Verdana", 11, FontStyle.Regular ), Brushes.DarkBlue, new PointF(45, 1));
graphics.DrawString(character4.ToString(), new Font("Arial" , 12, FontStyle.Underline), Brushes.DarkBlue, new PointF(65, 1));
Response.ContentType = "image/gif";
bitmap.Save(Response.OutputStream, ImageFormat.Gif);
bitmap.Dispose();
graphics.Dispose();
}
#endregion
}
}
▶ NoticeDeletePage.aspx
<%@ Page
Language="C#"
MasterPageFile="~/Site.Master"
AutoEventWireup="true"
CodeBehind="NoticeDeletePage.aspx.cs"
Inherits="TestProject.NoticeBoard.NoticeDeletePage"
Title="게시글 삭제" %>
<asp:Content ID="content" ContentPlaceHolderID="MainContent" runat="server">
<script>
function ConfirmDelete()
{
var canDelete = false;
if(window.confirm("현재 글을 삭제하시겠습니까?"))
{
canDelete = true;
}
else
{
canDelete = false;
}
return canDelete;
}
</script>
<h2 style="text-align: center;">게시판</h2>
<span style="color: #ff0000">글 삭제 - 글을 삭제하려면 글 작성시에 함께 저장한 패스워드가 필요합니다.</span>
<hr />
<div style="text-align:center;">
<asp:Label ID="idLabel" runat="server"
ForeColor="Red" />
번 글을 지우시겠습니까?
<br />
패스워드 :
<asp:TextBox ID="passwordTextBox" runat="server"
CssClass="form-control"
Style="display: inline-block;"
Width="120px"
TextMode="Password" />
<asp:Button ID="deleteButton" runat="server"
CssClass="btn btn-danger"
Style="display:inline-block;"
Width="100px"
Text="삭제"
OnClick="deleteButton_Click" />
<asp:HyperLink ID="cancelHyperLink" runat="server"
CssClass="btn btn-default">
취소
</asp:HyperLink>
<br />
<asp:Label ID="messageLabel" runat="server"
ForeColor="Red" />
<br />
</div>
</asp:Content>
▶ NoticeDeletePage.aspx.cs
using System;
using System.Web.UI;
using NoticeBoard.Models;
namespace TestProject.NoticeBoard
{
/// <summary>
/// 게시글 삭제 페이지
/// </summary>
public partial class NoticeDeletePage : Page
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Field
////////////////////////////////////////////////////////////////////////////////////////// Private
#region Field
/// <summary>
/// ID
/// </summary>
private string id;
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
this.id = Request.QueryString["ID"];
this.cancelHyperLink.NavigateUrl = "NoticeViewPage.aspx?ID=" + this.id;
this.idLabel.Text = this.id;
this.deleteButton.Attributes["onclick"] = "return ConfirmDelete();";
if(string.IsNullOrEmpty(this.id))
{
Response.Redirect("NoticeListPage.aspx");
}
}
#endregion
#region 삭제 버튼 클릭시 처리하기 - deleteButton_Click(sender, e)
/// <summary>
/// 삭제 버튼 클릭시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void deleteButton_Click(object sender, EventArgs e)
{
if((new NoticeRepository()).Delete(Convert.ToInt32(this.id), this.passwordTextBox.Text) > 0)
{
Response.Redirect("NoticeListPage.aspx");
}
else
{
this.messageLabel.Text = "삭제시 실패했습니다. 패스워드를 확인해 주시기 바랍니다.";
}
}
#endregion
}
}
▶ NoticeDownloadPage.aspx
<%@ Page
Language="C#"
AutoEventWireup="true"
CodeBehind="NoticeDownloadPage.aspx.cs"
Inherits="TestProject.NoticeBoard.NoticeDownloadPage" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
</head>
<body>
<form id="form" runat="server">
<div>
</div>
</form>
</body>
</html>
▶ NoticeDownloadPage.aspx.cs
using System;
using System.Web.UI;
using NoticeBoard.Models;
namespace TestProject.NoticeBoard
{
/// <summary>
/// 게시글 다운로드 페이지
/// </summary>
public partial class NoticeDownloadPage : Page
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Field
////////////////////////////////////////////////////////////////////////////////////////// Private
#region Field
/// <summary>
/// 업로드 디렉토리 경로
/// </summary>
private string uploadDirectoryPath = string.Empty;
/// <summary>
/// 파일명
/// </summary>
private string fileName = string.Empty;
/// <summary>
/// 게시글 저장소
/// </summary>
private NoticeRepository repository;
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
////////////////////////////////////////////////////////////////////////////////////////// Public
#region 생성자 - NoticeDownloadPage()
/// <summary>
/// 생성자
/// </summary>
public NoticeDownloadPage()
{
this.repository = new NoticeRepository();
}
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
this.uploadDirectoryPath = Server.MapPath("~/FileUpload/");
this.fileName = this.repository.GetFileName(Convert.ToInt32(Request["ID"]));
if(this.fileName == null)
{
Response.Clear();
Response.End();
}
else
{
this.repository.UpdateDownloadCount(this.fileName);
Response.Clear();
Response.ContentType = "application/octet-stream";
string attachmentFileName = Server.UrlPathEncode((this.fileName.Length > 50) ? this.fileName.Substring(this.fileName.Length - 50, 50) : this.fileName);
Response.AddHeader
(
"Content-Disposition",
"attachment;filename=" + attachmentFileName
);
Response.WriteFile(this.uploadDirectoryPath + this.fileName);
Response.End();
}
}
#endregion
}
}
▶ NoticeListPage.aspx
<%@ Page
Language="C#"
MasterPageFile="~/Site.Master"
AutoEventWireup="true"
CodeBehind="NoticeListPage.aspx.cs"
Inherits="TestProject.NoticeBoard.NoticeListPage"
Title="게시글 목록 페이지" %>
<%@ Register
Src="~/NoticeBoard/Controls/SearchControl.ascx"
TagPrefix="local"
TagName="SearchControl" %>
<%@ Register
Src="~/NoticeBoard/Controls/PagingControl.ascx"
TagPrefix="local"
TagName="PagingControl" %>
<asp:Content ID="content" ContentPlaceHolderID="MainContent" runat="server">
<h2 style="text-align:center;">게시판</h2>
<span style="color:#ff0000">글 목록</span>
<hr />
<table style="margin-left:auto;margin-right:auto;width:700px;">
<tr>
<td>
<style>
table th
{
text-align : center;
}
</style>
<div style="font-style:italic;text-align:right;font-size:8pt;">
전체 레코드 : <asp:Literal ID="totalRecordLabel" runat="server" />
</div>
<asp:GridView ID="gridView" runat="server"
CssClass="table table-bordered table-hover table-condensed table-striped table-responsive"
AutoGenerateColumns="False"
DataKeyNames="ID">
<Columns>
<asp:TemplateField HeaderText="ID"
HeaderStyle-Width="50px"
ItemStyle-HorizontalAlign="Center">
<ItemTemplate>
<%# RecordCount - ((Container.DataItemIndex)) - (PageIndex * 10) %>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="제목"
ItemStyle-HorizontalAlign="Left"
HeaderStyle-Width="350px">
<ItemTemplate>
<%# TestProject.NoticeBoardHelper.GetReplyImageHTML(Eval("ReplyLevel")) %>
<asp:HyperLink ID="lnkTitle" runat="server"
NavigateUrl='<%# "NoticeViewPage.aspx?ID=" + Eval("ID") %>'>
<%# TestProject.StringHelper.CutUnicodeString(Eval("Title").ToString(), 30) %>
</asp:HyperLink>
<%# TestProject.NoticeBoardHelper.GetReplyCountHTML(Eval("CommentCount")) %>
<%# TestProject.NoticeBoardHelper.GetNewImageHTML(Eval("WriteDate"))%>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="파일"
HeaderStyle-Width="70px"
ItemStyle-HorizontalAlign="Center">
<ItemTemplate>
<%# TestProject.NoticeBoardHelper.GetFileDownloadLinkHTML(Convert.ToInt32(Eval("ID")), Eval("FileName").ToString(), Eval("FileSize").ToString()) %>
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="Name" HeaderText="작성자"
HeaderStyle-Width="60px"
ItemStyle-HorizontalAlign="Center" />
<asp:TemplateField HeaderText="작성일"
ItemStyle-Width="90px"
ItemStyle-HorizontalAlign="Center">
<ItemTemplate>
<%# TestProject.NoticeBoardHelper.GetDateTimeHTML(Eval("WriteDate")) %>
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="ReadCount" HeaderText="조회수"
ItemStyle-HorizontalAlign="Right"
HeaderStyle-Width="60px" />
</Columns>
</asp:GridView>
</td>
</tr>
<tr>
<td style="text-align:center;">
<local:PagingControl ID="pagingControl" runat="server" />
</td>
</tr>
<tr>
<td style="text-align:right;">
<a class="btn btn-primary" href="NoticeWritePage.aspx">글쓰기</a>
</td>
</tr>
</table>
<local:SearchControl ID="searchControl" runat="server" />
</asp:Content>
▶ NoticeListPage.aspx.cs
using System;
using System.Web.UI;
using NoticeBoard.Models;
namespace TestProject.NoticeBoard
{
/// <summary>
/// 게시판 목록 페이지
/// </summary>
public partial class NoticeListPage : Page
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Field
////////////////////////////////////////////////////////////////////////////////////////// Public
#region Field
/// <summary>
/// 페이지 인덱스
/// </summary>
public int PageIndex = 0;
/// <summary>
/// 레코드 카운트
/// </summary>
public int RecordCount = 0;
#endregion
////////////////////////////////////////////////////////////////////////////////////////// Private
#region Field
/// <summary>
/// 게시글 저장소
/// </summary>
private NoticeRepository repository;
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Property
////////////////////////////////////////////////////////////////////////////////////////// Public
#region 검색 모드 여부 - SearchMode
/// <summary>
/// 검색 모드 여부
/// </summary>
public bool SearchMode { get; set; } = false;
#endregion
#region 검색 필드 - SearchField
/// <summary>
/// 검색 필드
/// </summary>
public string SearchField { get; set; }
#endregion
#region 검색 쿼리 - SearchQuery
/// <summary>
/// 검색 쿼리
/// </summary>
public string SearchQuery { get; set; }
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
////////////////////////////////////////////////////////////////////////////////////////// Public
#region 생성자 - NoticeListPage()
/// <summary>
/// 생성자
/// </summary>
public NoticeListPage()
{
this.repository = new NoticeRepository();
}
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
SearchMode = (!string.IsNullOrEmpty(Request.QueryString["SearchField"]) && !string.IsNullOrEmpty(Request.QueryString["SearchQuery"]));
if(SearchMode)
{
SearchField = Request.QueryString["SearchField"];
SearchQuery = Request.QueryString["SearchQuery"];
}
if(Request["Page"] != null)
{
PageIndex = Convert.ToInt32(Request["Page"]) - 1;
}
else
{
PageIndex = 0;
}
if(SearchMode == false)
{
RecordCount = this.repository.GetCount();
}
else
{
RecordCount = this.repository.GetCount(SearchField, SearchQuery);
}
this.totalRecordLabel.Text = RecordCount.ToString();
this.pagingControl.PageIndex = PageIndex;
this.pagingControl.RecordCount = RecordCount;
if(!Page.IsPostBack)
{
DisplayData();
}
}
#endregion
////////////////////////////////////////////////////////////////////////////////////////// Private
#region 데이터 표시하기 - DisplayData()
/// <summary>
/// 데이터 표시하기
/// </summary>
private void DisplayData()
{
if(SearchMode == false)
{
this.gridView.DataSource = this.repository.GetList(PageIndex);
}
else
{
this.gridView.DataSource = this.repository.Search(PageIndex, SearchField, SearchQuery);
}
this.gridView.DataBind();
}
#endregion
}
}
▶ NoticeReplyPage.aspx
<%@ Page
Language="C#"
MasterPageFile="~/Site.Master"
AutoEventWireup="true"
ValidateRequest="false"
CodeBehind="NoticeReplyPage.aspx.cs"
Inherits="TestProject.NoticeBoard.NoticeReplyPage"
Title="게시글 답변 페이지" %>
<%@ Register
Src="~/NoticeBoard/Controls/EditorControl.ascx"
TagPrefix="local"
TagName="EditorControl" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<local:EditorControl ID="editorControl" runat="server" />
</asp:Content>
▶ NoticeReplyPage.aspx.cs
using System;
using System.Web.UI;
using NoticeBoard.Models;
namespace TestProject.NoticeBoard
{
/// <summary>
/// 게시글 답변 페이지
/// </summary>
public partial class NoticeReplyPage : Page
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
this.editorControl.FormType = BoardWriteFormType.Reply;
}
#endregion
}
}
▶ NoticeUpdatePage.aspx
<%@ Page
Language="C#"
MasterPageFile="~/Site.Master"
AutoEventWireup="true"
ValidateRequest="false"
CodeBehind="NoticeUpdatePage.aspx.cs"
Inherits="TestProject.NoticeBoard.NoticeUpdatePage"
Title="게시글 수정 페이지" %>
<%@ Register
Src="~/NoticeBoard/Controls/EditorControl.ascx"
TagPrefix="local"
TagName="EditorControl" %>
<asp:Content ID="content" ContentPlaceHolderID="MainContent" runat="server">
<local:EditorControl ID="editorControl" runat="server" />
</asp:Content>
▶ NoticeUpdatePage.aspx.cs
using System;
using System.Web.UI;
using NoticeBoard.Models;
namespace TestProject.NoticeBoard
{
/// <summary>
/// 게시글 수정 페이지
/// </summary>
public partial class NoticeUpdatePage : Page
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
this.editorControl.FormType = BoardWriteFormType.Modify;
}
#endregion
}
}
▶ NoticeViewPage.aspx
<%@ Page
Language="C#"
MasterPageFile="~/Site.Master"
AutoEventWireup="true"
CodeBehind="NoticeViewPage.aspx.cs"
Inherits="TestProject.NoticeBoard.NoticeViewPage"
Title="게시글 보기 페이지" %>
<%@ Register
Src="~/NoticeBoard/Controls/CommentControl.ascx"
TagPrefix="local"
TagName="CommentControl" %>
<asp:Content ID="content" ContentPlaceHolderID="MainContent" runat="server">
<h2 style="text-align:center;">게시판</h2>
<span style="color:#ff0000">글 보기 - 현재 글에 대해서 수정 및 삭제를 할 수 있습니다. </span>
<hr />
<table style="margin-left:auto;margin-right:auto;width:700px;">
<tbody>
<tr style="background-color:#46698c;color:white;">
<td style="width:80px;text-align:right;height:35px;"><b style="font-size:18px">제목</b> :</td>
<td colspan="3">
<asp:Label ID="titleLabel" runat="server"
Width="100%"
Font-Bold="True"
Font-Size="18px" />
</td>
</tr>
<tr style="background-color:#efefef;">
<td class="text-right">ID :</td>
<td>
<asp:Label ID="idLabel" runat="server"
Width="84" />
</td>
<td class="text-right">메일 주소 :</td>
<td>
<asp:Label ID="mailAddressLabel" runat="server"
Width="100%" />
</td>
</tr>
<tr style="background-color:#efefef;">
<td class="text-right">성명 :</td>
<td>
<asp:Label ID="nameLabel" runat="server"
Width="100%" />
</td>
<td class="text-right">홈페이지 : </td>
<td>
<asp:Label ID="homepageLabel" runat="server"
Width="100%" />
</td>
</tr>
<tr style="background-color:#efefef;">
<td class="text-right">작성일 :</td>
<td>
<asp:Label ID="writeDateLabel" runat="server"
Width="100%" />
</td>
<td class="text-right">IP 주소 :</td>
<td>
<asp:Label ID="writeIPLabel" runat="server"
Width="100%" />
</td>
</tr>
<tr style="background-color:#efefef;">
<td class="text-right">조회수 :</td>
<td>
<asp:Label ID="readCountLabel" runat="server"
Width="100%" />
</td>
<td class="text-right">파일 :</td>
<td>
<asp:Label ID="fileLabel" runat="server"
Width="100%" />
</td>
</tr>
<tr>
<td colspan="4" style="padding:10px;">
<asp:Literal ID="imageLiteral" runat="server" />
<asp:Label ID="contentLabel" runat="server"
Width="100%"
Height="115px" />
</td>
</tr>
<tr>
<td colspan="4">
<hr />
</td>
</tr>
<tr>
<td colspan="4">
<local:CommentControl ID="commentControl" runat="server" />
</td>
</tr>
</tbody>
</table>
<div style="text-align:center;">
<asp:HyperLink ID="deleteHyperLink" runat="server"
CssClass="btn btn-default">
삭제
</asp:HyperLink>
<asp:HyperLink ID="updateHyperLink" runat="server"
CssClass="btn btn-default">
수정
</asp:HyperLink>
<asp:HyperLink ID="replyHyperLink" runat="server"
CssClass="btn btn-default">
답변
</asp:HyperLink>
<asp:HyperLink ID="listHyperLink" runat="server"
CssClass="btn btn-default"
NavigateUrl="NoticeListPage.aspx">
목록
</asp:HyperLink>
</div>
<asp:Label ID="errorLabel" runat="server"
ForeColor="Red"
EnableViewState="False" />
<br />
</asp:Content>
▶ NoticeViewPage.aspx.cs
using System;
using System.Web.UI;
using NoticeBoard.Models;
namespace TestProject.NoticeBoard
{
/// <summary>
/// 게시글 보기 페이지
/// </summary>
public partial class NoticeViewPage : Page
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Field
////////////////////////////////////////////////////////////////////////////////////////// Private
#region Field
/// <summary>
/// ID
/// </summary>
private string id;
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
this.deleteHyperLink.NavigateUrl = "NoticeDeletePage.aspx?ID=" + Request["ID"];
this.updateHyperLink.NavigateUrl = "NoticeUpdatePage.aspx?ID=" + Request["ID"];
this.replyHyperLink.NavigateUrl = "NoticeReplyPage.aspx?ID=" + Request["ID"];
this.id = Request.QueryString["ID"];
if(this.id == null)
{
Response.Redirect("./NoticeListPage.aspx");
}
if(!Page.IsPostBack)
{
DisplayData();
}
}
#endregion
////////////////////////////////////////////////////////////////////////////////////////// Private
#region 데이터 표시하기 - DisplayData()
/// <summary>
/// 데이터 표시하기
/// </summary>
private void DisplayData()
{
NoticeModel notice = (new NoticeRepository()).Get(Convert.ToInt32(this.id));
this.idLabel.Text = this.id;
this.nameLabel.Text = notice.Name;
this.mailAddressLabel.Text = string.Format("<a href=\"mailto:{0}\">{0}</a>", notice.MailAddress);
this.titleLabel.Text = notice.Title;
string content = notice.Content;
string encoding = notice.Encoding;
if(encoding == "Text")
{
this.contentLabel.Text = HTMLHelper.EncodeIncludingTabAndSpace(content);
}
else if(encoding == "Mixed")
{
this.contentLabel.Text = content.Replace("\r\n", "<br />");
}
else
{
this.contentLabel.Text = content;
}
this.readCountLabel.Text = notice.ReadCount.ToString();
this.homepageLabel.Text = string.Format("<a href=\"{0}\" target=\"_blank\">{0}</a>", notice.Homepage);
this.writeDateLabel.Text = notice.WriteDate.ToString();
this.writeIPLabel.Text = notice.WriteIP;
if(notice.FileName.Length > 1)
{
this.fileLabel.Text = string.Format
(
"<a href='./NoticeDownloadPage.aspx?ID={0}'>{1} {2} / 전송 수 : {3}</a>",
notice.ID,
"<img src=\"/images/ext/ext_zip.gif\" border=\"0\">",
notice.FileName,
notice.DownloadCount
);
if(NoticeBoardHelper.IsImageFile(notice.FileName))
{
this.imageLiteral.Text = $"<img src=\'ImageDownloadPage.aspx?FileName={Server.UrlEncode(notice.FileName)}\'>";
}
}
else
{
this.fileLabel.Text = "(업로드된 파일이 없습니다.)";
}
}
#endregion
}
}
▶ NoticeWritePage.aspx
<%@ Page
Language="C#"
MasterPageFile="~/Site.Master"
AutoEventWireup="true"
ValidateRequest="false"
CodeBehind="NoticeWritePage.aspx.cs"
Inherits="TestProject.NoticeBoard.NoticeWritePage"
Title="게시글 쓰기 페이지" %>
<%@ Register
Src="~/NoticeBoard/Controls/EditorControl.ascx"
TagPrefix="local"
TagName="EditorControl" %>
<asp:Content ID="content" ContentPlaceHolderID="MainContent" runat="server">
<local:EditorControl id="editorControl" runat="server" />
</asp:Content>
▶ NoticeWritePage.aspx.cs
using System;
using System.Web.UI;
using NoticeBoard.Models;
namespace TestProject.NoticeBoard
{
/// <summary>
/// 게시글 쓰기 페이지
/// </summary>
public partial class NoticeWritePage : Page
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 페이지 로드시 처리하기 - Page_Load(sender, e)
/// <summary>
/// 페이지 로드시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
protected void Page_Load(object sender, EventArgs e)
{
this.editorControl.FormType = BoardWriteFormType.Write;
}
#endregion
}
}
728x90
반응형
그리드형(광고전용)
'C# > ASP.NET' 카테고리의 다른 글
[C#/ASP.NET] "...bin/roslyn/csc.exe" 경로의 일부를 찾을 수 없습니다 (0) | 2022.09.06 |
---|---|
[C#/ASP.NET] 누겟 설치 : SignalR (0) | 2020.10.03 |
[C#/ASP.NET] FormsAuthentication 클래스 : 사용자 로그인 관리하기 (0) | 2020.09.30 |
[C#/ASP.NET] Chart 클래스 사용하기 (0) | 2020.09.30 |
[C#/ASP.NET] ObjectDataSource 클래스 사용하기 (0) | 2020.09.29 |
[C#/ASP.NET] XmlDataSource 클래스 사용하기 (0) | 2020.09.29 |
[C#/ASP.NET] DAPPER CRUD 작업하기 (0) | 2020.09.29 |
[C#/ASP.NET] 데이터베이스 CRUD 작업하기 (0) | 2020.09.29 |
[C#/ASP.NET] Page 클래스 : Session 속성을 사용해 일정 시간 내 글쓰기 방지하기 (0) | 2020.09.29 |
[C#/ASP.NET] ASP.NET 상태 관리하기 (0) | 2020.09.29 |
댓글을 달아 주세요