티스토리 뷰

반응형

지난번 강좌에 사진 촬영에 대한 내용을 다루었다..그러나 사진 촬영만 하면 무었을 하겠는가..어디다가 저장을 해야하는데..물론 실버라이트는 기본적으로 격리저장소(Isolated Storage)를 제공하고 있어서, 처음에는 사진을 촬영해서 그곳에 저장하고 그걸 다시 서버로 전송을 할려는 계획을 세우고 이것 저것 찾아보았는데..

서버로 파일을 업로드할려고 찾았던 컨트롤이 절대 경로를 가지고 파일 정보를 가지고 올 수 있어야지만, 업로드가 가능했던 것이다.하지만, 격리저장소는 OOB 상태가 아니라면 실버라이트4 응용프로그램이 절대 경로를 사용해서는 접근이 불가능하다..(이런 내용을 알기위해 삽질했던 시간이 2틀 정도였던 것 같다..ㅡㅡ;;;)

그렇다면, 사진을 찍어서 로컬에 저장하지 않고, 그냥 서버로 바로 던져서 서버에 저장을 해야겠다는 결론을 내리고 사이트를 검색하다가 찾은 것이 WCF RIA Service를 사용한 파일 업로드 방식이였다. 처음에 그걸 발견하고, 완전 기뻐서 땡큐를 연발하면서 리플을 남겼었다.

강좌를 위해서 소스를 좀 수정하고, 간단하게 목록까지 볼 수 있도록 수정을 했다.

이 화면의 구성은 왼쪽 리스트 박스에서 클릭을 하면 오른쪽에 큰 이미지를 보여 줄려는 의도로 만들었다. 그러나..시간 관계상..해당 기능을 구현하지는 않았다. 여러분들께서 만들어서 올려 줄것이라고 믿는다. ^^ 또..사진 촬영을 2번하면 화면이 먹통이 되는 이상 현상이 있는데..이 부분도 수정해서 완성 올려주면 좋겠다. 그래서 구현되어 있는 기능은

1) 웹켐 촬영

2) 촬영된 이미지를 Jpeg로 변환

3) 변환된 이미지를 WCF RIA Service를 이용해서 서버로 전송

4) 서버에 이미지 저장

5) 저장된 이미지 목록 WCF RIA Service로 조회

6) 조회된 파일 목록을 가지고 리스트박스에 뿌려주면서 실제 이미지를 웹서버에서 불러와서 보여준다.


1. 서버작업

SL4_RIA_Sample.Web 프로젝트에

Assets 폴더를 하나 추가
FileHandler.cs, WebFile.cs를 추가한다.

WebFile.cs

using System.ComponentModel.DataAnnotations;

namespace SL4_RIA_Sample.Web.Assets
{
    //전송될 파일 클래스
    public class WebFile
    {  //WCF RIA Service의 DTO로 사용하기 위해서는 반드시 Key가 필요하다.
        [Key]
        public string FileName { get; set; }
        public byte[] FileContent { get; set; }
    }
}

무지 심플한 클래스다. 이 클래스가 파일을 마치 레코드 다루듯이 가지고 올것이다.

FileHandler.cs

using System.Web;
using System.Web.Configuration;
using System.IO;

namespace SL4_RIA_Sample.Web.Assets
{
    /// <summary>
    /// http://community.infragistics.com/blogs/anton_staykov/archive/2010/04/28/implementing-simple-file-upload-using-silverlight-4-drag-amp-drop-feature-and-infragistics-compression-library.aspx
    /// 이곳에 있던 소스를 참고로 수정
    /// 파일핸들러 스택틱 클래스 웹파일을 로컬(서버)에 저장하는 역할을 한다.
    /// </summary>
    public static class FileHandler
    {
        /// <summary>
        /// 파일전송 함수
        /// </summary>
        /// <param name="file">웹파일 클래스</param>
        public static void HandleFile(WebFile file)
        {
            //업로드 폴더 지정(Web.Config 파일에 절대 경로 존재)
            string uploadDir = WebConfigurationManager.AppSettings["UploadDir"];
            //업로드 폴더가 존재하면
            if (!string.IsNullOrEmpty(uploadDir))
            {
                //업로드 폴더명에 ~/ 이런 글씨가 있으면
                if (uploadDir.IndexOf("~/") == 0)
                    //상대경로를 절대경로로 변경
                    uploadDir = HttpContext.Current.Server.MapPath(uploadDir);
                //업로드 폴더명에 /이것만 있으면
                if (uploadDir.LastIndexOf("/") == uploadDir.Length - 1)
                    //절대경로이니 대충 짤라서 사용
                    uploadDir = uploadDir.Substring(0, uploadDir.Length - 1);

                //저장할 풀경로를 만들고
                string fullFileName = string.Format("{0}/{1}", uploadDir, file.FileName);

                //파일 존재여부 확인
                if (File.Exists(fullFileName))
                {
                    //확장자
                    string ext = fullFileName.Substring(fullFileName.LastIndexOf("."));
                    //파일명
                    string fName = fullFileName.Substring(0, fullFileName.LastIndexOf("."));
                    //풀파일명
                    fullFileName = string.Format("{0}_1{1}", fName, ext);
                }
                //저장~
                File.WriteAllBytes(fullFileName, file.FileContent);
            }
        }
    }
}

위의 소스를 보니 Web.config에 업로드 폴더도 지정해야한다.

Web.config 파일에 추가
  <!--업로드폴더 지정-->
  <appSettings>
    <add key="UploadDir" value="~/UserUploads" />
  </appSettings>

음..그럼 이걸 사용하는 WCF RIA Service를 만들자

FileUploaderDomainService.cs

namespace SL4_RIA_Sample.Web
{
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.ServiceModel.DomainServices.Hosting;
    using System.ServiceModel.DomainServices.Server;
    using System.Web;
    using System.Web.Configuration;
    using IDApp.Web.Assets;

    /// <summary>
    /// 파일 업로드용 WCF RIA Service 생성
    /// </summary>
    [EnableClientAccess()]
    public class FileUploaderDomainService : DomainService
    {
        //파일 목록들 조회
        public IQueryable<WebFile> GetFiles()
        {
            //업로드 폴더명 가지고 가서
            string path = WebConfigurationManager.AppSettings["UploadDir"];

            //웹파일 정보 리스트 객체를 만들고,
            List<WebFile> webFiles = new List<WebFile>();

            //폴더에 내용이 없으면 그냥 빈거 던지고
            if (string.IsNullOrEmpty(path))
                return webFiles.AsQueryable();

            //디렉토리 정보를 가지고 오고
            DirectoryInfo di = new DirectoryInfo(HttpContext.Current.Server.MapPath(path));
            //파일정보도 가지고 오고
            foreach (FileInfo file in di.GetFiles())
            {
                //웹파일 정보 리스트에 하나씩 추가하고
                webFiles.Add(new WebFile { FileName = file.Name });
            }
            //결과를 던진다.
            return webFiles.AsQueryable();
        }

        //파일 업로드 함수
        public void InsertFile(WebFile file)
        {
            FileHandler.HandleFile(file);
        }
    }
}

마지막으로 프로젝트에 UserUploads 폴더를 추가해 놓는다.
여기 까지 작업을 하면 서버쪽 작업은 일단 완료된다. 컴파일을 해서 오류가 있는지 확인해 본다. 오류가 없으면 클라이언트쪽 작업을 시작한다.


2. 클라이언트 작업

일단 사진을 촬영하면 BitmapImage으로 만들어 진다. 우린 이걸 Jpeg로 변환해야하는데 이미지 컨버트용 오픈소스 라이브러리를 사용한다.

FluxJpeg 라는 라이브러리(https://github.com/briandonahue/FluxJpeg.Core)
위의 주소를 가면 전체 소스를 Download를 받을 수 있고, 다운로드를 받아서 소스를 VS2010으로 열고, 빌드를 해주면 FJ.Core.dll이란 파일이 만들어 진다. 그 DLL파일을 프로젝트에 추가해서 사용한다. 일단 이 소스에도 해당 Dll파일이 포함되어져 있다. Bin\Debug 폴더를 보면 FJ.Core.dll파일을 찾을 수 있다. 파일을 폴더에 복사해 놓은 후 Add Reference로 해당 dll을 프로젝트 리퍼런스로 추가해 놓는다. 지금 추가한 라이브러리를 편하게 사용하기 위해서 확장메소드 기능을 사용한다.


Extension.cs
이 확장메소드의 소스는 복사해 온것이기 때문에..주석이 없다..

using System.IO;
using System.Windows.Media.Imaging;
using FluxJpeg.Core;
using FluxJpeg.Core.Encoder;

namespace SL4_RIA_Sample
{
    //확장메소드 만들기
    public static class Extension
    {
        public static void EncodeJpeg(this WriteableBitmap bitmap, MemoryStream stream, int quality = 90)
        {
            int width = bitmap.PixelWidth;
            int height = bitmap.PixelHeight;
            int bands = 3;
            byte[][,] raster = new byte[bands][,];
            for (int i = 0; i < bands; i++)
            {
                raster[i] = new byte[width, height];
            }
            for (int row = 0; row < height; row++)
            {
                for (int column = 0; column < width; column++)
                {
                    int pixel = bitmap.Pixels[width * row + column];
                    raster[0][column, row] = (byte)(pixel >> 16); //R
                    raster[1][column, row] = (byte)(pixel >> 8);  //G
                    raster[2][column, row] = (byte)pixel;         //B
                }
            }
            ColorModel model = new ColorModel { colorspace = ColorSpace.RGB };
            FluxJpeg.Core.Image img = new FluxJpeg.Core.Image(model, raster);

            JpegEncoder encoder = new JpegEncoder(img, quality, stream);
            encoder.Encode();

            stream.Seek(0, SeekOrigin.Begin);
        }
    }
}

확장메소드가 무엇인지..어떻게 사용하는지는 잠시 후에 보도록 하겠다.

확장메소드까지 작업을 했다면..사진을 보여주는 화면을 보도록 하자..

PhotoView.xaml

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:SL4_RIA_Sample_Web_DomainService="clr-namespace:SL4_RIA_Sample.Web"
    xmlns:local="clr-namespace:SL4_RIA_Sample" x:Class="SL4_RIA_Sample.PhotoView"
    mc:Ignorable="d" Loaded="UserControl_Loaded"
    d:DesignHeight="300" d:DesignWidth="400">

    <UserControl.Resources>
        <local:ImageFullPathConverter x:Key="ImageFullPathConverter"/>

        <!--리스트박스에서 사용하는 데이터 템플릿-->
        <DataTemplate x:Key="ListDataTemplate">
            <Grid d:DesignWidth="97" d:DesignHeight="99">
                <Grid.RowDefinitions>
                    <RowDefinition Height="0.192*"/>
                    <RowDefinition Height="0.808*"/>
                </Grid.RowDefinitions>
                <TextBlock TextWrapping="Wrap" Text="{Binding FileName}"/>
                <Viewbox Grid.Row="1" HorizontalAlignment="Left">
                    <Image Source="{Binding FileName, Converter={StaticResource ImageFullPathConverter}}"
                           Width="120" Margin="5"/>
                </Viewbox>
            </Grid>
        </DataTemplate>
    </UserControl.Resources>
   
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="177" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="38" />
            <RowDefinition Height="261*" />
        </Grid.RowDefinitions>
       
        <Grid.DataContext>

            <!--블랜드에서 바인딩 작업 할려고 추가해 놓았던 것-->
            <SL4_RIA_Sample_Web_DomainService:FileUploaderDomainContext/>
        </Grid.DataContext>

        <!--상단 버튼-->       
        <StackPanel Orientation="Horizontal" Grid.ColumnSpan="2">
            <Button Content="웹켐 촬영" Width="80" Margin="4" Click="Button_Click" />
            <Button Content="이미지 목록" Width="80" Margin="4" Click="Button_Click_1" />
        </StackPanel>

        <!--리스트박스-->
        <ListBox Grid.Row="1"
                 ItemTemplate="{StaticResource ListDataTemplate}"
                 ItemsSource="{Binding WebFiles, Mode=OneWay}"
                 HorizontalContentAlignment="Stretch"
                 VerticalContentAlignment="Stretch"/>
       
    </Grid>
</UserControl>


PhotoView.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using SL4_RIA_Sample.Web;

 

namespace SL4_RIA_Sample
{
    public partial class PhotoView : UserControl
    {
        //WCF RIA Service 컨텍스트 생성
        FileUploaderDomainContext ctx;

 

        public PhotoView()
        {
            InitializeComponent();
        }

 

        /// <summary>
        /// 촬영시작 버튼
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            WebCam_Child webCam = new WebCam_Child();
            webCam.Show();
        }

        /// <summary>
        /// 유저컨트롤 로드 완료 이벤트
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {

            //초기화
            ctx = new FileUploaderDomainContext();

            //DataContext에 도메인서비스 자체를 넣어놓음 (물론 좋은 방법은 아니니..옵저블 컬렉션을 사용 권고)

            LayoutRoot.DataContext = ctx;
        }

 

        /// <summary>
        /// 이미지 목록 버튼
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            ctx.Load(ctx.GetFilesQuery(), true);
        }
    }
}

여기까지 작업을 했으면 중요한 한가지가 있다. 리스트박스에 이미지 이름을 바인딩하는데 이미지 이름 바인딩 하면서 바로 ImageFullPathConverter가 필요한데..여기서 하는 일이 중요하다.


ImageFullPathConverter.cs

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Data;

 

namespace SL4_RIA_Sample
{
    public class ImageFullPathConverter : IValueConverter
    {

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            string fileName = value as string;
            string ReturnValue;

            //호스트 Uri구함
            string hostUri = App.Current.Host.Source.ToString().Replace(App.Current.Host.Source.LocalPath, "");
            ReturnValue = hostUri + "/UserUploads/" + fileName;

            return ReturnValue;
        }


        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

이미지들은 서버에 존재하기 때문에 서버의 주소를 이미지 이름 앞에 추가해 주는 기능을 한다.

그럼 이제 마지막으로


WebCam_Child.xaml.cs 파일을 수정해 보자


using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.IO;
using SL4_RIA_Sample.Web;
using SL4_RIA_Sample.Web.Assets;
using System.ServiceModel.DomainServices.Client;

namespace SL4_RIA_Sample
{
    public partial class WebCam_Child : ChildWindow
    {
        private CaptureSource _capture;
        //일단 여기다가 저장해놨다가, 성공하면 프로퍼티로 이동
        private string captureFileName;
        //다른 화면에서 저장된 파일명 참고
        public string CaptureFileName;


        public WebCam_Child()
        {
            InitializeComponent();
        }

        private void ChildWindow_Loaded(object sender, RoutedEventArgs e)
        {
            //캡춰소스 인스턴스
            _capture = new CaptureSource();
            _capture.CaptureImageCompleted += new EventHandler<CaptureImageCompletedEventArgs>(_capture_CaptureImageCompleted);
        }

        /// <summary>
        /// 이미지 캡처 완료되었을 때 발생하는 이벤트
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void _capture_CaptureImageCompleted(object sender, CaptureImageCompletedEventArgs e)
        {
            //이미지 캡춰가 완료되면 여기로
            WriteableBitmap wb = e.Result;
            //화면에 촬영된 이미지 보여주기
            if (_capture != null)
            {
                _capture.Stop();
                ImageBrush image = new ImageBrush();
                image.ImageSource = wb;
                rectVideo.Fill = image;
            }

            //파일명 생성
            string name = Guid.NewGuid().ToString() + ".bmp";
            //메모리 스트림 인스턴스
            MemoryStream stream = new MemoryStream();

            //확장메소드를 이용해서 이미지를 jpeg 포멧으로 변환
            wb.EncodeJpeg(stream);

            //WCF RIA Service 컨텍스트 생성
            FileUploaderDomainContext ctx = new FileUploaderDomainContext();
            //웹파일 정보 하나 만들고
            WebFile file = new WebFile { FileName = name };
            //파일 컨텐츠에 메모리 스트림의 버퍼데이터를 쓸어 넣는다.
            //메모리 스트림아니라 파일 스트림의 데이터도 넣을 수 있다.
            file.FileContent = stream.GetBuffer();
            //웹파일스에..방금 만든거 추가..마치 레코드 추가하듯이
            ctx.WebFiles.Add(file);
            //그리고 세이브체인지 호출~
            ctx.SubmitChanges(SubmitCallBack, null);
            //전송한 파일 이름을 저장해 놓고
            captureFileName = name;
        }

        /// <summary>
        /// 세이브체인지 완료 콜백함수
        /// </summary>
        /// <param name="op"></param>
        private void SubmitCallBack(SubmitOperation op)
        {
            //에러가 있으면 에러 보여주고 끝~
            if (op.HasError)
            {
                MessageBox.Show(op.Error.Message);
                this.DialogResult = false;
                CaptureFileName = "";
            }
            else
            { //여긴 저장 완료~
                MessageBox.Show("작업을 완료했습니다.");
                this.DialogResult = true;
                CaptureFileName = captureFileName;
            }
            //팝업 화면 닫기
            this.Close();
        }

        private void OKButton_Click(object sender, RoutedEventArgs e)
        {
            //캡춰 시작
            _capture.CaptureImageAsync();
        }

        private void CancelButton_Click(object sender, RoutedEventArgs e)
        {
            this.DialogResult = false;
        }

        private void StartButton_Click(object sender, RoutedEventArgs e)
        {
            Button btn = ((Button)sender);

            if (_capture != null)
            {
                //일단 동작 정지
                _capture.Stop();

                if (btn.Content.ToString() == "촬영시작")
                {
                    //기본 비디오 디바이스를 가지고옴
                    _capture.VideoCaptureDevice = CaptureDeviceConfiguration.GetDefaultVideoCaptureDevice();
                    //기본 오디오 디바이스를 가지고옴
                    _capture.AudioCaptureDevice = CaptureDeviceConfiguration.GetDefaultAudioCaptureDevice();

                    //비디오 브러쉬 만들고
                    VideoBrush videoBrush = new VideoBrush();
                    videoBrush.Stretch = Stretch.Uniform;
                    videoBrush.SetSource(_capture);
                    rectVideo.Fill = videoBrush;
                    // request user permission and display the capture
                    if (CaptureDeviceConfiguration.AllowedDeviceAccess || CaptureDeviceConfiguration.RequestDeviceAccess())
                    {
                        _capture.Start();
                        btn.Content = "촬영종료";
                    }
                }
                else
                {
                    btn.Content = "촬영시작";
                }
            }
        }
    }
}

 

3. 간단하게 만들려고 했는데..
일이 커져버렸다..;; 배보다 배꼽이 더 커진..흐흐..서버로 파일을 업로드 하는 방법도 이렇게 사용해서 올릴 수 있다. 어떤 파일이라도 스트림형태로 읽어 들인다면, 그 스트림의 버퍼를 WebFile 클래스에 쑤셔 넣어서 SubmitChange()만 호출 한다면 자동으로 올라간다..앞으로 실버라이트에서 WCF RIA Service를 사용한 완성된 파일 업로드 기능을 기대하며 이번 강좌를 마친다.

반응형
댓글