첨부 실행 코드는 나눔고딕코딩 폰트를 사용합니다.
본 블로그는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 블로그 콘텐츠 향상을 위해 쓰여집니다.

728x90
반응형
728x170

TestProject.zip
다운로드

▶ SmoothingFilterType.cs

namespace TestProject
{
    /// <summary>
    /// 스무딩 필터 타입
    /// </summary>
    public enum SmoothingFilterType
    {
        /// <summary>
        /// None
        /// </summary>
        None,

        /// <summary>
        /// Gaussian 3X3
        /// </summary>
        Gaussian3X3,

        /// <summary>
        /// Gaussian 5X5
        /// </summary>
        Gaussian5X5,

        /// <summary>
        /// Gaussian 7X7
        /// </summary>
        Gaussian7X7,

        /// <summary>
        /// Median 3X3
        /// </summary>
        Median3X3,

        /// <summary>
        /// Median 5X5
        /// </summary>
        Median5X5,

        /// <summary>
        /// Median 7X7
        /// </summary>
        Median7X7,

        /// <summary>
        /// Median 9X9
        /// </summary>
        Median9X9,

        /// <summary>
        /// Mean 3X3
        /// </summary>
        Mean3X3,

        /// <summary>
        /// Mean 5X5
        /// </summary>
        Mean5X5,

        /// <summary>
        /// Low Pass 3X3
        /// </summary>
        LowPass3X3,

        /// <summary>
        /// Low Pass 5X5
        /// </summary>
        LowPass5X5,

        /// <summary>
        /// Sharpen 3X3
        /// </summary>
        Sharpen3X3,
    }
}

 

728x90

 

▶ Matrix.cs

namespace TestProject
{
    /// <summary>
    /// 매트릭스
    /// </summary>
    public static class Matrix
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Public

        #region Gaussian 3X3 - Gaussian3X3

        /// <summary>
        /// Gaussian 3X3
        /// </summary>
        public static double[,] Gaussian3X3
        {
            get
            {
                return new double[,]
                {
                    { 1, 2, 1 },
                    { 2, 4, 2 },
                    { 1, 2, 1 }
                };
            }
        }

        #endregion
        #region Gaussian 5X5 - Gaussian5X5

        /// <summary>
        /// Gaussian 5X5
        /// </summary>
        public static double[,] Gaussian5X5
        {
            get
            {
                return new double[,]
                {
                    { 2,  4,  5,  4, 2 },
                    { 4,  9, 12,  9, 4 },
                    { 5, 12, 15, 12, 5 },
                    { 4,  9, 12,  9, 4 },
                    { 2,  4,  5,  4, 2 }
                };
            }
        }

        #endregion
        #region Gaussian 7X7 - Gaussian7X7

        /// <summary>
        /// Gaussian 7X7
        /// </summary>
        public static double[,] Gaussian7X7
        {
            get
            {
                return new double[,]
                {
                    { 1, 1, 2,  2, 2, 1, 1 },
                    { 1, 2, 2,  4, 2, 2, 1 },
                    { 2, 2, 4,  8, 4, 2, 2 },
                    { 2, 4, 8, 16, 8, 4, 2 },
                    { 2, 2, 4,  8, 4, 2, 2 },
                    { 1, 2, 2,  4, 2, 2, 1 },
                    { 1, 1, 2,  2, 2, 1, 1 }
                };
            }
        }

        #endregion
        #region Mean 3X3 - Mean3X3

        /// <summary>
        /// Mean 3X3
        /// </summary>
        public static double[,] Mean3X3
        {
            get
            {
                return new double[,]
                {
                    { 1, 1, 1 },
                    { 1, 1, 1 },
                    { 1, 1, 1 }
                };
            }
        }

        #endregion
        #region Mean 5X5 - Mean5X5

        /// <summary>
        /// Mean 5X5
        /// </summary>
        public static double[,] Mean5X5
        {
            get
            {
                return new double[,]
                {
                    { 1, 1, 1, 1, 1 },
                    { 1, 1, 1, 1, 1 },
                    { 1, 1, 1, 1, 1 },
                    { 1, 1, 1, 1, 1 },
                    { 1, 1, 1, 1, 1 }
                };
            }
        }

        #endregion
        #region Low Pass 3X3 - LowPass3X3

        /// <summary>
        /// Low Pass 3X3
        /// </summary>
        public static double [,] LowPass3X3
        {
            get
            {
                return new double [,]
                {
                    { 1, 2, 1 },
                    { 2, 4, 2 },
                    { 1, 2, 1 }
                };
            }
        }

        #endregion
        #region Low Pass 5X5 - LowPass5X5

        /// <summary>
        /// Low Pass 5X5
        /// </summary>
        public static double[,] LowPass5X5
        {
            get
            {
                return new double[,]
                {
                    { 1, 1,  1, 1, 1 },
                    { 1, 4,  4, 4, 1 },
                    { 1, 4, 12, 4, 1 },
                    { 1, 4,  4, 4, 1 },
                    { 1, 1,  1, 1, 1 }
                };
            }
        }

        #endregion
        #region Sharpen 3X3 - Sharpen3X3

        /// <summary>
        /// Sharpen 3X3
        /// </summary>
        public static double[,] Sharpen3X3
        {
            get
             {
                return new double[,]
                {
                    { -1, -2, -1 },
                    {  2,  4,  2 },
                    {  1,  2,  1 }
                };
             }
        }

        #endregion
    }
}

 

300x250

 

▶ BitmapHelper.cs

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

namespace TestProject
{
    /// <summary>
    /// 비트맵 헬퍼
    /// </summary>
    public static class BitmapHelper
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Public

        #region 비트맵 로드하기 - LoadBitmap(filePath)

        /// <summary>
        /// 비트맵 로드하기
        /// </summary>
        /// <param name="filePath">파일 경로</param>
        /// <returns>비트맵</returns>
        public static Bitmap LoadBitmap(string filePath)
        {
            using(Bitmap bitmap = new Bitmap(filePath))
            {
                return new Bitmap(bitmap);
            }
        }

        #endregion
        #region 카툰 필터 적용하기 - ApplyCartoonEffectFilter(sourceBitmap, threshold, filterType)

        /// <summary>
        /// 카툰 필터 적용하기
        /// </summary>
        /// <param name="sourceBitmap">소스 비트맵</param>
        /// <param name="threshold">임계치</param>
        /// <param name="filterType">스무딩 필터 타입</param>
        /// <returns>비트맵</returns>
        public static Bitmap ApplyCartoonEffectFilter
        (
            Bitmap              sourceBitmap,
            byte                threshold  = 0,
            SmoothingFilterType filterType = SmoothingFilterType.None
        )
        {
            sourceBitmap = ApplySmoothingFilter(sourceBitmap, filterType);

            BitmapData sourceBitmapData = sourceBitmap.LockBits
            (
                new Rectangle (0, 0, sourceBitmap.Width, sourceBitmap.Height),
                ImageLockMode.ReadOnly,
                PixelFormat.Format32bppArgb
            );

            byte[] sourceByteArray = new byte[sourceBitmapData.Stride * sourceBitmapData.Height];
            byte[] targetByteArray = new byte[sourceBitmapData.Stride * sourceBitmapData.Height];

            Marshal.Copy(sourceBitmapData.Scan0, sourceByteArray, 0, sourceByteArray.Length);

            sourceBitmap.UnlockBits(sourceBitmapData);

            int    byteOffset    = 0;
            int    blueGradient  = 0;
            int    greenGradient = 0;
            int    redGradient   = 0;
            double blue          = 0;
            double green         = 0;
            double red           = 0;

            bool exceedsThreshold = false;

            for(int offsetY = 1; offsetY < sourceBitmap.Height - 1; offsetY++)
            {
                for(int offsetX = 1; offsetX < sourceBitmap.Width - 1; offsetX++)
                {
                    byteOffset = offsetY * sourceBitmapData.Stride + offsetX * 4;

                    blueGradient = Math.Abs(sourceByteArray[byteOffset - 4] - sourceByteArray[byteOffset + 4]);

                    blueGradient += Math.Abs
                    (
                        sourceByteArray[byteOffset - sourceBitmapData.Stride] -
                        sourceByteArray[byteOffset + sourceBitmapData.Stride]
                    );

                    byteOffset++;

                    greenGradient = Math.Abs(sourceByteArray[byteOffset - 4] - sourceByteArray[byteOffset + 4]);

                    greenGradient += Math.Abs
                    (
                        sourceByteArray[byteOffset - sourceBitmapData.Stride] -
                        sourceByteArray[byteOffset + sourceBitmapData.Stride]
                    );

                    byteOffset++;

                    redGradient = Math.Abs(sourceByteArray[byteOffset - 4] - sourceByteArray[byteOffset + 4]);

                    redGradient += Math.Abs
                    (
                        sourceByteArray[byteOffset - sourceBitmapData.Stride] -
                        sourceByteArray[byteOffset + sourceBitmapData.Stride]
                    );

                    if(blueGradient + greenGradient + redGradient > threshold)
                    {
                        exceedsThreshold = true ;
                    }
                    else
                    {
                        byteOffset -= 2;

                        blueGradient = Math.Abs(sourceByteArray[byteOffset - 4] - sourceByteArray[byteOffset + 4]);

                        byteOffset++;

                        greenGradient = Math.Abs(sourceByteArray[byteOffset - 4] - sourceByteArray[byteOffset + 4]);

                        byteOffset++;

                        redGradient = Math.Abs(sourceByteArray[byteOffset - 4] - sourceByteArray[byteOffset + 4]);

                        if(blueGradient + greenGradient + redGradient > threshold)
                        {
                            exceedsThreshold = true ;
                        }
                        else
                        {
                            byteOffset -= 2;

                            blueGradient = Math.Abs
                            (
                                sourceByteArray[byteOffset - sourceBitmapData.Stride] -
                                sourceByteArray[byteOffset + sourceBitmapData.Stride]
                            );

                            byteOffset++;

                            greenGradient = Math.Abs
                            (
                                sourceByteArray[byteOffset - sourceBitmapData.Stride] -
                                sourceByteArray[byteOffset + sourceBitmapData.Stride]
                            );

                            byteOffset++;

                            redGradient = Math.Abs
                            (
                                sourceByteArray[byteOffset - sourceBitmapData.Stride] -
                                sourceByteArray[byteOffset + sourceBitmapData.Stride]
                            );

                            if(blueGradient + greenGradient + redGradient > threshold)
                            {
                                exceedsThreshold = true ;
                            }
                            else
                            {
                                byteOffset -= 2;

                                blueGradient = Math.Abs
                                (
                                    sourceByteArray[byteOffset - 4 - sourceBitmapData.Stride] -
                                    sourceByteArray[byteOffset + 4 + sourceBitmapData.Stride]
                                );

                                blueGradient += Math.Abs
                                (
                                    sourceByteArray[byteOffset - sourceBitmapData.Stride + 4] -
                                    sourceByteArray[byteOffset + sourceBitmapData.Stride - 4]
                                );

                                byteOffset++;

                                greenGradient = Math.Abs
                                (
                                    sourceByteArray[byteOffset - 4 - sourceBitmapData.Stride] -
                                    sourceByteArray[byteOffset + 4 + sourceBitmapData.Stride]
                                );

                                greenGradient += Math.Abs
                                (
                                    sourceByteArray[byteOffset - sourceBitmapData.Stride + 4] -
                                    sourceByteArray[byteOffset + sourceBitmapData.Stride - 4]
                                );

                                byteOffset++;

                                redGradient = Math.Abs
                                (
                                    sourceByteArray[byteOffset - 4 - sourceBitmapData.Stride] -
                                    sourceByteArray[byteOffset + 4 + sourceBitmapData.Stride]
                                );

                                redGradient += Math.Abs
                                (
                                    sourceByteArray[byteOffset - sourceBitmapData.Stride + 4] -
                                    sourceByteArray[byteOffset + sourceBitmapData.Stride - 4]
                                );

                                if(blueGradient + greenGradient + redGradient > threshold)
                                {
                                    exceedsThreshold = true ;
                                }
                                else
                                {
                                    exceedsThreshold = false ;
                                }
                            }
                        }
                    }

                    byteOffset -= 2;

                    if(exceedsThreshold)
                    {
                        blue  = 0;
                        green = 0;
                        red   = 0;
                    }
                    else
                    {
                        blue  = sourceByteArray[byteOffset    ];
                        green = sourceByteArray[byteOffset + 1];
                        red   = sourceByteArray[byteOffset + 2];
                    }

                    blue  = (blue  > 255 ? 255 : (blue  < 0 ? 0 : blue ));
                    green = (green > 255 ? 255 : (green < 0 ? 0 : green));
                    red   = (red   > 255 ? 255 : (red   < 0 ? 0 : red  ));

                    targetByteArray[byteOffset    ] = (byte)blue;
                    targetByteArray[byteOffset + 1] = (byte)green;
                    targetByteArray[byteOffset + 2] = (byte)red;
                    targetByteArray[byteOffset + 3] = 255;
                }
            }

            Bitmap targetBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height);

            BitmapData targetBitmapData = targetBitmap.LockBits
            (
                new Rectangle(0, 0, targetBitmap.Width, targetBitmap.Height),
                ImageLockMode.WriteOnly,
                PixelFormat.Format32bppArgb
            );

            Marshal.Copy(targetByteArray, 0, targetBitmapData.Scan0, targetByteArray.Length);

            targetBitmap.UnlockBits(targetBitmapData);

            return targetBitmap;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////// Private

        #region 중앙값 필터 적용하기 - ApplyMedianFilter(sourceBitmap, matrixSize)

        /// <summary>
        /// 중앙값 필터 적용하기
        /// </summary>
        /// <param name="sourceBitmap">소스 비트맵</param>
        /// <param name="matrixSize">매트릭스 크기</param>
        /// <returns>비트맵</returns>
        private static Bitmap ApplyMedianFilter(Bitmap sourceBitmap, int matrixSize)
        {
            BitmapData sourceBitmapData = sourceBitmap.LockBits
            (
                new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height),
                ImageLockMode.ReadOnly,
                PixelFormat.Format32bppArgb
            );

            byte[] sourceByteArray = new byte[sourceBitmapData.Stride * sourceBitmapData.Height];
            byte[] targetByteArray = new byte[sourceBitmapData.Stride * sourceBitmapData.Height];

            Marshal.Copy(sourceBitmapData.Scan0, sourceByteArray, 0, sourceByteArray.Length);

            sourceBitmap.UnlockBits(sourceBitmapData);

            int filterOffset = (matrixSize - 1) / 2;

            int temporaryOffset = 0;

            int byteOffset = 0;

            List<int> neighbourPixelList = new List<int>();

            byte[] middlePixel;

            for(int offsetY = filterOffset; offsetY < sourceBitmap.Height - filterOffset; offsetY++)
            {
                for(int offsetX = filterOffset; offsetX < sourceBitmap.Width - filterOffset; offsetX++)
                {
                    byteOffset = offsetY * sourceBitmapData.Stride + offsetX * 4;

                    neighbourPixelList.Clear();

                    for(int filterY = -filterOffset; filterY <= filterOffset; filterY++)
                    {
                        for(int filterX = -filterOffset; filterX <= filterOffset; filterX++)
                        {
                            temporaryOffset = byteOffset + (filterX * 4) + (filterY * sourceBitmapData.Stride);

                            neighbourPixelList.Add(BitConverter.ToInt32(sourceByteArray, temporaryOffset));
                        }
                    }

                    neighbourPixelList.Sort();

                    middlePixel = BitConverter.GetBytes(neighbourPixelList[filterOffset]);

                    targetByteArray[byteOffset    ] = middlePixel[0];
                    targetByteArray[byteOffset + 1] = middlePixel[1];
                    targetByteArray[byteOffset + 2] = middlePixel[2];
                    targetByteArray[byteOffset + 3] = middlePixel[3];
                }
            }

            Bitmap targetBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height);

            BitmapData targetBitmapData = targetBitmap.LockBits
            (
                new Rectangle(0, 0, targetBitmap.Width, targetBitmap.Height),
                ImageLockMode.WriteOnly,
                PixelFormat.Format32bppArgb
            );

            Marshal.Copy(targetByteArray, 0, targetBitmapData.Scan0, targetByteArray.Length);

            targetBitmap.UnlockBits(targetBitmapData);

            return targetBitmap;
        }

        #endregion
        #region 회선 필터 적용하기 - ApplyConvolutionFilter(sourceBitmap, filterArray, factor, bias)

        /// <summary>
        /// 회선 필터 적용하기
        /// </summary>
        /// <param name="sourceBitmap">소스 비트맵</param>
        /// <param name="filterArray">필터 배열</param>
        /// <param name="factor">인자</param>
        /// <param name="bias">바이어스</param>
        /// <returns>비트맵</returns>
        private static Bitmap ApplyConvolutionFilter(Bitmap sourceBitmap, double[,] filterArray, double factor = 1, int bias = 0)
        {
            BitmapData sourceBitmapData = sourceBitmap.LockBits
            (
                new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height),
                ImageLockMode.ReadOnly,
                PixelFormat.Format32bppArgb
            );

            byte[] sourceByteArray = new byte[sourceBitmapData.Stride * sourceBitmapData.Height];
            byte[] targetByteArray = new byte[sourceBitmapData.Stride * sourceBitmapData.Height];

            Marshal.Copy(sourceBitmapData.Scan0, sourceByteArray, 0, sourceByteArray.Length);

            sourceBitmap.UnlockBits(sourceBitmapData);

            double blue  = 0.0;
            double green = 0.0;
            double red   = 0.0;

            int filterWidth  = filterArray.GetLength(1);
            int filterHeight = filterArray.GetLength(0);
            int filterOffset = (filterWidth - 1) / 2;
            int sourceOffset = 0;
            int targetOffset = 0;

            for(int offsetY = filterOffset; offsetY < sourceBitmap.Height - filterOffset; offsetY++)
            {
                for(int offsetX = filterOffset; offsetX < sourceBitmap.Width - filterOffset; offsetX++)
                {
                    blue  = 0;
                    green = 0;
                    red   = 0;

                    targetOffset = offsetY * sourceBitmapData.Stride + offsetX * 4;

                    for(int filterY = -filterOffset; filterY <= filterOffset; filterY++)
                    {
                        for(int filterX = -filterOffset; filterX <= filterOffset; filterX++)
                        {
                            sourceOffset = targetOffset + (filterX * 4) + (filterY * sourceBitmapData.Stride);

                            blue += (double)(sourceByteArray[sourceOffset]) * 
                                    filterArray[filterY + filterOffset, filterX + filterOffset];

                            green += (double)(sourceByteArray[sourceOffset + 1]) *
                                        filterArray[filterY + filterOffset, filterX + filterOffset];

                            red += (double)(sourceByteArray[sourceOffset + 2]) *
                                    filterArray[filterY + filterOffset, filterX + filterOffset];
                        }
                    }

                    blue  = factor * blue + bias;
                    green = factor * green + bias;
                    red   = factor * red + bias;

                    blue  = (blue  > 255 ? 255 : (blue  < 0 ? 0 : blue ));
                    green = (green > 255 ? 255 : (green < 0 ? 0 : green));
                    red   = (red   > 255 ? 255 : (red   < 0 ? 0 : red  ));

                    targetByteArray[targetOffset    ] = (byte)(blue);
                    targetByteArray[targetOffset + 1] = (byte)(green);
                    targetByteArray[targetOffset + 2] = (byte)(red);
                    targetByteArray[targetOffset + 3] = 255;
                }
            }

            Bitmap targetBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height);

            BitmapData targetBitmapData = targetBitmap.LockBits
            (
                new Rectangle(0, 0, targetBitmap.Width, targetBitmap.Height),
                ImageLockMode.WriteOnly,
                PixelFormat.Format32bppArgb
            );

            Marshal.Copy(targetByteArray, 0, targetBitmapData.Scan0, targetByteArray.Length);

            targetBitmap.UnlockBits(targetBitmapData);

            return targetBitmap;
        }

        #endregion
        #region 스무딩 필터 적용하기 - ApplySmoothingFilter(sourceBitmap, filterType)

        /// <summary>
        /// 스무딩 필터 적용하기
        /// </summary>
        /// <param name="sourceBitmap">소스 비트맵</param>
        /// <param name="filterType">스무딩 필터 타입</param>
        /// <returns>비트맵</returns>
        private static Bitmap ApplySmoothingFilter(Bitmap sourceBitmap, SmoothingFilterType filterType = SmoothingFilterType.None)
        {
            Bitmap targetBitmap = null;

            switch(filterType)
            {
                case SmoothingFilterType.None :

                    targetBitmap = sourceBitmap;

                    break;

                case SmoothingFilterType.Gaussian3X3 :

                    targetBitmap = ApplyConvolutionFilter(sourceBitmap, Matrix.Gaussian3X3, 1.0 / 16.0, 0);

                    break;

                case SmoothingFilterType.Gaussian5X5 :

                    targetBitmap = ApplyConvolutionFilter(sourceBitmap, Matrix.Gaussian5X5, 1.0 / 159.0, 0);

                    break;

                case SmoothingFilterType.Gaussian7X7 :

                    targetBitmap = ApplyConvolutionFilter(sourceBitmap, Matrix.Gaussian7X7, 1.0 / 136.0, 0);

                    break;

                case SmoothingFilterType.Median3X3 :

                    targetBitmap = ApplyMedianFilter(sourceBitmap, 3);

                    break;

                case SmoothingFilterType.Median5X5 :

                    targetBitmap = ApplyMedianFilter(sourceBitmap, 5);

                    break;

                case SmoothingFilterType.Median7X7 :

                    targetBitmap = ApplyMedianFilter(sourceBitmap, 7);

                    break;

                case SmoothingFilterType.Median9X9 :

                    targetBitmap = ApplyMedianFilter(sourceBitmap, 9);

                    break;

                case SmoothingFilterType.Mean3X3 :

                    targetBitmap = ApplyConvolutionFilter(sourceBitmap, Matrix.Mean3X3, 1.0 / 9.0, 0);

                    break;

                case SmoothingFilterType.Mean5X5 :

                    targetBitmap = ApplyConvolutionFilter(sourceBitmap, Matrix.Mean5X5, 1.0 / 25.0, 0);

                    break;

                case SmoothingFilterType.LowPass3X3 :

                    targetBitmap = ApplyConvolutionFilter(sourceBitmap, Matrix.LowPass3X3, 1.0 / 16.0, 0);

                    break;

                case SmoothingFilterType.LowPass5X5 :

                    targetBitmap = ApplyConvolutionFilter(sourceBitmap, Matrix.LowPass5X5, 1.0 / 60.0, 0);

                    break;

                case SmoothingFilterType.Sharpen3X3 :

                    targetBitmap = ApplyConvolutionFilter(sourceBitmap, Matrix.Sharpen3X3, 1.0 / 8.0, 0);

                    break;
            }

            return targetBitmap;
        }

        #endregion
    }
}

 

▶ MainForm.cs

using System.Drawing;
using System.Windows.Forms;

namespace TestProject
{
    /// <summary>
    /// 메인 폼
    /// </summary>
    public partial class MainForm : Form
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - MainForm()

        /// <summary>
        /// 생성자
        /// </summary>
        public MainForm()
        {
            InitializeComponent();

            Bitmap sourceBitmap = BitmapHelper.LoadBitmap("IMAGE\\sample.jpg");
            Bitmap targetBitmap = BitmapHelper.ApplyCartoonEffectFilter(sourceBitmap, 128, SmoothingFilterType.Gaussian3X3);

            this.pictureBox.SizeMode = PictureBoxSizeMode.Zoom;
            this.pictureBox.Image    = targetBitmap;
        }

        #endregion
    }
}
728x90
반응형
그리드형(광고전용)
Posted by 사용자 icodebroker

댓글을 달아 주세요