1. 개요
페이스북의 계정별 FEED를 별도의 개발자 인증 과정 없이 손쉽게 읽기 위한 방법을 델파이로 구현한다.
2. 알아야 할 것들.
- 페이스북 Feed를 읽기 위한 AccessToken 얻기
개인별 페이스북 계정에 로그인 한 후에 다음 URL로 접근한다.
https://developers.facebook.com/tools/explorer/145634995501895/?method=GET&path=feedname%2Ffeed%3Ffields%3Did%2Cname%2Cmessage%2Clink%2Ccaption%2Csource%2Cpermalink_url%26limit%3D100&version=v2.6 * feedname에는 수집하고자 하는 feed명을 넣어 주면 된다. * 예를 들어 삼성전자 페이스북 FEED명은 sumsung 이다. |
- AccessToken 확인과 Feed를 API를 이용해 호출한 결과를 Json data로 확인이 가능하다.
- 수집대상 필드 조정하기 : 좌측에 "Search for a field"를 클릭하여 feed json data에 추가될 필드를 지정한다.
- 엑세스토큰(AccessToken) : 피드 API를 호출하려면 AccessToken을 얻어야 하는데 위 화면을 리프레시 하면 AccessToken을 새로 발급 받을 수 있으며 테스트로 호출된 Json 페이지를 호출하는 API는 하단 "코드받기"를 클릭하여 개발 어플리케이션별 활용 가능한 API를 확인 할 수 있다.
- 그림에서는 http로 호출하기 위한 URL을 얻을 수 있다.
3. DB-테이블 스키마(MSSQL)
- TABLE : facebook_data
drop table facebook_data create table facebook_data ( SN numeric primary key identity(1,1), [SID] int null, CONFIRM int, GDATE DateTime, SeasonID Int, FB_ID nvarchar(60), FB_NAME nvarchar(100), FB_FROM_NAME nvarchar(100), FB_FROM_ID nvarchar(100), FB_LINK nvarchar(1000), FB_PERMALINK_URL nvarchar(1000), FB_CREATED_TIME datetime, FB_UPDATED_TIME datetime, FB_FULL_PICTURE nvarchar(1000), FB_CONTENTS NVARCHAR(MAX), FB_DESCRIPTION NVARCHAR(MAX), FB_STORY NVARCHAR(MAX), FB_MESSAGE NVARCHAR(MAX), ) create index IDX_SID on facebook_data ([SID]) create index IDX_FB_ID on facebook_data (FB_ID) select * from facebook_data
- StoredProcedure : usp_facebook_insert : feed 데이터 삽입
ALTER PROCEDURE [dbo].[usp_facebook_insert] @SN numeric output , @SID int , @CONFIRM int , @GDATE datetime , @SeasonID int , @FB_ID nvarchar(60) , @FB_NAME nvarchar(100) , @FB_FROM_NAME nvarchar(100) , @FB_FROM_ID nvarchar(100) , @FB_LINK nvarchar(1000) , @FB_PERMALINK_URL nvarchar(1000) , @FB_CREATED_TIME datetime , @FB_UPDATED_TIME datetime , @FB_FULL_PICTURE nvarchar(1000) , @FB_CONTENTS nvarchar(max) , @FB_DESCRIPTION nvarchar(2000) , @FB_STORY nvarchar(1000) , @FB_MESSAGE nvarchar(2000) AS BEGIN SET NOCOUNT ON; IF (@SN=0) IF EXISTS(SELECT SN FROM facebook_data WHERE FB_ID=@FB_ID) SELECT @SN=SN FROM facebook_data WHERE FB_ID=@FB_ID IF (@SN=0) -- INSERT MODE BEGIN INSERT INTO facebook_data ([SID], CONFIRM, GDATE, SeasonID, FB_ID, FB_NAME, FB_FROM_NAME, FB_FROM_ID, FB_LINK, FB_PERMALINK_URL, FB_CREATED_TIME, FB_UPDATED_TIME, FB_FULL_PICTURE, FB_CONTENTS, FB_DESCRIPTION, FB_STORY, FB_MESSAGE) VALUES (@SID, @CONFIRM, @GDATE, @SeasonID, @FB_ID, @FB_NAME, @FB_FROM_NAME, @FB_FROM_ID, @FB_LINK, @FB_PERMALINK_URL, @FB_CREATED_TIME, @FB_UPDATED_TIME, @FB_FULL_PICTURE, @FB_CONTENTS, @FB_DESCRIPTION, @FB_STORY, @FB_MESSAGE) SET @SN=@@IDENTITY END ELSE UPDATE facebook_data SET [SID]= @SID , CONFIRM=@CONFIRM , GDATE=@GDATE , SeasonID=@SeasonID , FB_ID=@FB_ID , FB_NAME=@FB_NAME , FB_FROM_NAME=@FB_FROM_NAME , FB_FROM_ID=@FB_FROM_ID , FB_LINK=@FB_LINK , FB_PERMALINK_URL=@FB_PERMALINK_URL , FB_CREATED_TIME=@FB_CREATED_TIME , FB_UPDATED_TIME=@FB_UPDATED_TIME , FB_FULL_PICTURE=@FB_FULL_PICTURE , FB_CONTENTS=@FB_CONTENTS , FB_DESCRIPTION=@FB_DESCRIPTION , FB_STORY=@FB_STORY , FB_MESSAGE=@FB_MESSAGE WHERE SN = @SN END
- StoredProcedure : usp_facebook_update : feeddata 갱신
ALTER PROCEDURE [dbo].[usp_facebook_update] @SN numeric , @SID int , @CONFIRM int , @GDATE datetime , @SEASONID int , @FB_ID nvarchar(60) , @FB_NAME nvarchar(100) , @FB_FROM_NAME nvarchar(100) , @FB_FROM_ID nvarchar(100) , @FB_LINK nvarchar(1000) , @FB_PERMALINK_URL nvarchar(1000) , @FB_CREATED_TIME datetime , @FB_UPDATED_TIME datetime , @FB_FULL_PICTURE nvarchar(1000) , @FB_CONTENTS nvarchar(max) , @FB_DESCRIPTION nvarchar(2000) , @FB_STORY nvarchar(1000) , @FB_MESSAGE nvarchar(2000) AS BEGIN SET NOCOUNT ON; UPDATE facebook_data SET [SID]= @SID , CONFIRM=@CONFIRM , GDATE=@GDATE , SeasonID=@SeasonID , FB_ID=@FB_ID , FB_NAME=@FB_NAME , FB_FROM_NAME=@FB_FROM_NAME , FB_FROM_ID=@FB_FROM_ID , FB_LINK=@FB_LINK , FB_PERMALINK_URL=@FB_PERMALINK_URL , FB_CREATED_TIME=@FB_CREATED_TIME , FB_UPDATED_TIME=@FB_UPDATED_TIME , FB_FULL_PICTURE=@FB_FULL_PICTURE , FB_CONTENTS=@FB_CONTENTS , FB_DESCRIPTION=@FB_DESCRIPTION , FB_STORY=@FB_STORY , FB_MESSAGE=@FB_MESSAGE WHERE SN = @SN END
- StoredProcedure : usp_facebook_getlist : feed data얻기
CREATE PROCEDURE [dbo].[usp_facebook_getlist] @SID int, @SeasonID int = -1, @Confirm int = -1, @Top int = 0 AS BEGIN SET NOCOUNT ON; DECLARE @WHERE VARCHAR(100) DECLARE @SQL VARCHAR(200) SET @WHERE = 'SID=' + STR(@SID) IF (@SeasonID>-1) SET @WHERE = @WHERE + ' AND SeasonID=' + STR(@SeasonID) IF (@Confirm>-1) SET @WHERE = @WHERE + ' AND Confirm=' + STR(@Confirm) IF (@Top>0) SET @SQL = 'SELECT TOP '+STR(@Top) + ' * FROM facebook_data WHERE ' + @WHERE ELSE SET @SQL = 'SELECT * FROM facebook_data WHERE ' + @WHERE SET @SQL = @SQL + ' ORDER BY SN ASC' exec sp_executesql @SQL END
4. FEED 수집 프로세스
가. Facebook에 사용자 계정으로 로그인 한다.
나. https://developers.facebook.com/tools feed툴 페이지에 접근하여 AccessToken을 얻는다.
다. Feed샘플을 확인하고 코드 얻기를 통해 Get방식의 URL 정보를 얻고 적당히 가공한다. (요청 필드 목록들 추가)
라. 얻어진 URL에서 AccessToken만을 바꿔가며 매번 수집한다(AccessToken은 발급 받은지 1시간 경과하면 Expired된다)
마. 얻어진 json 데이터를 파싱하여 각각의 레코드로 기록한다.
바. 페이지 네비게이션을 위해서는 json 데이터 하단의 "next" value를 얻어서 다음페이지 네비게이션을 반복 한다.
* 특정날짜 이후는 URL에 "since=2016-01-01"과 같은 형식으로 적어준다.
* 한번에 얻을 수 있는 Acticle의 최대(limit) 갯수는 100개 까지이며 limit=100 처럼 적어준다.
5. FACEBOOK Feed 수집을 위한 델파이 소스
- 참고로 데이터 객체의 부모클래스인 "TbnContent"는 JsonToObject / ObjectToJson 으로 json 데이터를 읽고 쓸 수 있는 기능이 구현되어 있다.
- 웹 Request는 인디의 TIdHTTP 컴포넌트를 이용하였다.
- 필드 : SID는 수집피드 정보원 ID쯤으로 생각하면 된다.
- TFacebookArticled의 private 섹션의 중요 필드들은 실제 Json data내의 필드명과 일치 시켜야 객체의 JsonToObject를 통해 자동으로 객체화 시킬 수 있다.
- 예약어 등으로 필드명을 일치시키지 못할 경우에는 수신한 json data의 소스를 "srcfield" --> "destfield"로 사전 변경처리하는 과정이 필요하다.
unit uFacebookData; interface uses Classes, SysUtils, uCommonData, Json, RTTI, uDebug, IdHTTP, DB, ADODB; const URL_FACEBOOK_AUTH = 'https://developers.facebook.com/tools/explorer/145634995501895/?method=GET&path=gyeonkorea%2Ffeed%3Fsince%3D2016-01-01%26limit%3D100&version=v2.6'; ERROR_CODE = 'OAuthException'; FEED_DOMAIN = 'https://graph.facebook.com/v2.6/'; DEFAULT_ACCESS_TOKEN = 'EAAN9hkawNL8BABZBjpVbUqtTbGe9PVllwipa0FCKTtWGdZBMIaMdNfdltZBSrihmJMY1Tyf1jHRXuFWYiRHvrUmllI7x5I6UDy4ZB9lyXAN5aoRG50kCZB4sXI5mpfkGZBp4b4i9M47UZACUImo9E2fNACW6ZBrQB61ESAdEbfLDigZDZD'; DEFAULT_PARAMS = '/feed?since=@SINCE&fields=caption%2Cdescription%2Cfull_picture%2Cid%2Cfrom%2Clink%2Cmessage%2Cname%2Cpermalink_url%2Cstory%2Ccreated_time%2Cupdated_time&limit=100&access_token='; type TFacebookUser = class(TbnContent) private //페북 글쓴 아이디 fbname: String; //페북 글쓴 이름 id: String; public end; TFacebookArticle = class(TbnContent) private //JSONData에 의해서 자동으로 읽히는 부분 //페북 아티클 아이디 id : String; //페북 아티클 작성자 정보 from : TFacebookUser; //이름 fbname : String; //페북 아티클 바로가기 링크 link : String; //페북 아티클 바로가기 링크(영구적인) permalink_url : String; //페북 아티클 생성일시 created_time : TDateTime; //페북 아티클 수정일시 updated_time : TDateTime; //페북 이미지 풀경로 full_picture : String; //페북 아티클 메시지(원본은 message 필드인데 델파이 예약어로 소스에서 contents로 변경 처리 contents : string; //설명글 description : String; //스로리 story : String; //메시지 fbmessage : String; //JSONDATA와 무관한 정보 //페북단위 정보원 ID FSID: Integer; //수집일련번호 FSn: Int64; //수집일시 FGDate: TDateTime; //예약필드 - 검수확인 FConfirm : Integer; //수집 회차번호 - 회차를 달리하여 새로 수집될 수 있다. FSeasonID: Integer; procedure Read(Asds: TDataSet); override; procedure Write(Asds: TDataSet); override; public property Sn : Int64 read FSn write FSn; property SID : Integer read FSID write FSID; property GDate : TDateTime read FGDate write FGDate; property Confirm : Integer read FConfirm write FConfirm; property SeasonID : Integer read FSeasonID write FSeasonID; function ToString : string; override; procedure Insert; override; procedure Delete; override; procedure Update; override; constructor Create(ASQLConn: TUnivConnection); override; destructor Destroy; override; procedure JSONToObject(AJSONString: String); override; end; TFacebookData = class(TbnContentList) private FSID: Integer; function _GetFacebookArticle(nIdx: Integer): TFacebookArticle; procedure _SetSID(const Value: Integer); public property SID : Integer read FSID write _SetSID; property Items[nIdx : Integer] : TFacebookArticle read _GetFacebookArticle; procedure LoadData(ASID : Integer; ATop : Integer = 0; ASeasonID : Integer = -1; AConfirm : Integer = -1); procedure Save; end; TFacebookCollector = class(TIdHTTP) private FFeed: String; FFacebookData: TFacebookData; FSID: Integer; function _GetURL: String; public property Feed : String read FFeed write FFeed; property FacebookData : TFacebookData read FFacebookData write FFacebookData; property URL : String read _GetURL; constructor Create(AOwner : TComponent; AFacebookData : TFaceBookData; AFeed : String); overload; procedure GetFacebookData(ASID : Integer; ABeginDate : TDateTime; AAccessToken : String); end; procedure ParseJsonForFacebook(AJsonData : String; AFaceBookData : TFacebookData); procedure ParseJsonFileForFacebook(AJsonFile : String; AFaceBookData : TFacebookData); implementation uses uStringManager; procedure ParseJsonFileForFacebook(AJsonFile : String; AFaceBookData : TFacebookData); var sl : TStringList; begin try sl := TStringList.Create; sl.LoadFromFile(AJsonFile); ParseJsonForFacebook(sl.Text, AFaceBookData); finally sl.Free; end; end; procedure ParseJsonForFacebook(AJsonData : String; AFaceBookData : TFacebookData); var jsonString : String; begin try jsonString := StringReplace(AJsonData, '"name"', '"fbname"', [rfReplaceAll]); jsonString := StringReplace(jsonString, '"message"', '"fbmessage"', [rfReplaceAll]); AFaceBookData.JSONToObject(jsonString, TFacebookArticle, 'data', false); finally end; end; { TFacebookData } procedure TFacebookData._SetSID(const Value: Integer); var i : integer; begin FSID := Value; for i := 0 to count-1 do Items[i].SID := SID; end; procedure TFacebookData.LoadData(ASID, ATop, ASeasonID, AConfirm: Integer); var objFB : TFacebookArticle; ads : TUnivStoredProc; begin ClearAll; ads := TBaseContent.CreateStoredProc(self.SQLCon); try ads.ProcedureName := 'usp_facebook_getlist'; ads.ClearParams; ads.CreateParameter('@SID', ftInteger, pdInput, SizeOf(integer), ASID); ads.CreateParameter('@SeasonID', ftInteger, pdInput, SizeOf(integer), ASeasonID); ads.CreateParameter('@Confirm', ftInteger, pdInput, SizeOf(integer), AConfirm); ads.CreateParameter('@Top', ftInteger, pdInput, SizeOf(integer), ATop); ads.Open; while not ads.Eof do begin objFB := TFacebookArticle.Create(SQLCon); objFB.Read(ads); Add(objFB); ads.Next; end; finally ads.Free; end; end; procedure TFacebookData.Save; var i : integer; objFB : TFacebookArticle; begin for i := 0 to count-1 do begin objFB := Items[i]; objFB.Insert; // AutoUpdate end; end; function TFacebookData._GetFacebookArticle(nIdx: Integer): TFacebookArticle; begin result := TFacebookArticle(Get(nIdx)); end; { TFacebookArticle } constructor TFacebookArticle.Create(ASQLConn: TUnivConnection); begin inherited; from := TFacebookUser.Create(SQLConn); GDate := now; end; procedure TFacebookArticle.Delete; begin inherited; end; destructor TFacebookArticle.Destroy; begin from.Free; inherited; end; procedure TFacebookArticle.Insert; var ads : TUnivStoredProc; begin if sn>0 then Update else begin ads := TBaseContent.CreateStoredProc(SQLConn); try ads.ProcedureName := 'usp_facebook_insert'; Write(ads); ads.ExecProc; sn := ads.Parameters.ParamByName('@SN').Value; finally ads.Free; end; end; end; function TFacebookArticle.ToString: string; var sl : TStringList; begin sl := TStringList.Create; sl.Add('SN = ' + intToStr(SN)); sl.Add('SID = ' + intToStr(SID)); sl.Add('CONFIRM = ' + intToStr(Confirm)); sl.Add('GDATE = ' + formatDateTime('yyyy-mm-dd', GDate)); sl.Add('id = ' + id); sl.Add('link = ' + link); sl.Add('fbname = ' + fbname); sl.Add('from.id = ' + from.id); sl.Add('from.fbname = ' + from.fbname); sl.Add('created_time = ' + formatDateTime('yyyy-mm-dd hh:nn:ss', created_time)); sl.Add('updated_time = ' + formatDateTime('yyyy-mm-dd hh:nn:ss', updated_time)); sl.Add('permalink = ' + permalink_url); sl.Add('contents = ' + contents); sl.Add('description = ' + description); sl.Add('story = ' + story); sl.Add('message = ' + fbmessage); result := sl.Text; sl.free; end; procedure TFacebookArticle.Update; var ads : TUnivStoredProc; begin if sn=0 then Insert else begin ads := TBaseContent.CreateStoredProc(SQLConn); try ads.ProcedureName := 'usp_facebook_update'; Write(ads); ads.ExecProc; finally ads.Free; end; end; end; procedure TFacebookArticle.Write(Asds: TDataSet); var univAds : TUnivStoredProc; begin univAds := TUnivStoredProc(Asds); univAds.ClearParams; univAds.CreateParameter('@SN', ftInteger, pdInputOutput, SizeOf(int64), Sn); univAds.CreateParameter('@SID', ftInteger, pdInput, SizeOf(integer), SID); univAds.CreateParameter('@CONFIRM', ftInteger, pdInput, SizeOf(integer), Confirm); univAds.CreateParameter('@GDATE', ftDateTime, pdInput, SizeOf(TDateTime), GDate); univAds.CreateParameter('@SEASONID', ftInteger, pdInput, SizeOf(integer), SeasonID); univAds.CreateParameter('@FB_ID', ftWideString, pdInput, 60, id); univAds.CreateParameter('@FB_NAME', ftWideString, pdInput, 100, fbName); univAds.CreateParameter('@FB_FROM_NAME', ftWideString, pdInput, 100, from.fbname); univAds.CreateParameter('@FB_FROM_ID', ftWideString, pdInput, 100, from.id); univAds.CreateParameter('@FB_LINK', ftWideString, pdInput, 1000, link); univAds.CreateParameter('@FB_PERMALLINK_URL', ftWideString, pdInput, 1000, permalink_url); univAds.CreateParameter('@FB_CREATED_TIME', ftDateTime, pdInput, SizeOf(TDateTime), created_time); univAds.CreateParameter('@FB_UPDATED_TIME', ftDateTime, pdInput, SizeOf(TDateTime), updated_time); univAds.CreateParameter('@FB_FULL_PICTURE', ftWideString, pdInput, 1000, full_picture); univAds.CreateParameter('@FB_CONTENTS', ftWideMemo, pdInput, 16, ''); univAds.CreateParameter('@FB_DESCRIPTION', ftWideMemo, pdInput, 16, ''); univAds.CreateParameter('@FB_STORY', ftWideMemo, pdInput, 16, ''); univAds.CreateParameter('@FB_MESSAGE', ftWideMemo, pdInput, 16, ''); univAds.Parameters.ParamByName('@FB_CONTENTS').Value := contents; univAds.Parameters.ParamByName('@FB_DESCRIPTION').Value := description; univAds.Parameters.ParamByName('@FB_STORY').Value := story; univAds.Parameters.ParamByName('@FB_MESSAGE').Value := fbmessage; end; procedure TFacebookArticle.JSONToObject(AJSONString: String); var RttiContext: TRttiContext; RttiType: TRttiType; FieldCount : Integer; RttiField : TRttiField; JsonObject : TJsonObject; JsonValue : TJsonValue; JsonArray : TJsonArray; JsonPair : TJsonPair; PairCount : Integer; begin if AJSONString='' then exit; TRACE('ClassType : ' + self.ClassName); //현재 객체의 런타임 클래스 타입 정보를 얻는다. RttiType := RttiContext.GetType(self.ClassType); //전달받은 JSON문자열을 JSON객체로 파싱한다. JsonObject := TJSONObject.ParseJSONValue(AJSONString) as TJsonObject; try //클래스의 필드를 순회 하면서 각 필드를 처리한다. for RttiField in RttiType.GetFields do begin try JsonValue := JsonObject.GetValue(RttiField.Name) as TJSONValue; if RttiField.Name='from' then from.JSONToObject(JsonValue.ToString) else begin if JsonValue<>nil then SetRttiFieldValue(RttiField, JsonValue); end; except on e:Exception do begin TRACE('Error : RttiField.Name=' + RttiField.Name + ', JsonValue.Value=' + JsonValue.Value + #13+ e.Message); end; end; end; finally JsonObject.Free; end; end; procedure TFacebookArticle.Read(Asds: TDataSet); var univAds : TUnivStoredProc; begin univAds := TUnivStoredProc(Asds); Sn := univAds.FieldByName('SN').AsInteger; SID := univAds.FieldByName('SID').AsInteger; Confirm := univAds.FieldByName('Confirm').AsInteger; SeasonID := univAds.FieldByName('SeasonID').AsInteger; id := univAds.FieldByName('FB_ID').AsString; fbname := univAds.FieldByName('FB_NAME').AsString; from.fbname := univAds.FieldByName('FB_FROM_NAME').AsString; from.id := univAds.FieldByName('FB_FROM_ID').AsString; link := univAds.FieldByName('FB_LINK').AsString; permalink_url := univAds.FieldByName('FB_PERMALINK_URL').AsString; created_time := univAds.FieldByName('FB_CREATED_TIME').AsDateTime; updated_time := univAds.FieldByName('FB_UPDATED_TIME').AsDateTime; full_picture := univAds.FieldByName('FB_FULL_PICTURE').AsString; contents := univAds.FieldByName('FB_CONTENTS').AsString; description := univAds.FieldByName('FB_DESCRIPTION').AsString; story := univAds.FieldByName('FB_STORY').AsString; fbmessage := univAds.FieldByName('FB_MESSAGE').AsString; end; { TFacebookCollector } constructor TFacebookCollector.Create(AOwner: TComponent; AFacebookData: TFaceBookData; AFeed : String); begin inherited Create(AOwner); FacebookData := AFacebookData; Feed := AFeed; end; procedure TFacebookCollector.GetFacebookData(ASID : Integer; ABeginDate: TDateTime; AAccessToken: String); var runURL : String; responseStream : TStringStream; i : integer; begin runURL := StringReplace(URL + AAccessToken, '@SINCE', formatDateTime('yyyy-mm-dd', ABeginDate), []); FacebookData.ClearAll; responseStream := TStringStream.Create; try i := 0; while runURL<>'' do begin inc(i); responseStream.Clear; Get(runURL, responseStream); ParseJsonForFacebook(responseStream.DataString, FacebookData); runURL := sm.mid('"next":"', '"', responseStream.DataString); runURL := StringReplace(runURL, '\/', '/', [rfReplaceAll]); FacebookData.SID := ASID; //해주어야만 모든 객체에 SID 값이 전파 된다. FacebookData.Save; end; finally responseStream.free; end; end; function TFacebookCollector._GetURL: String; begin result := FEED_DOMAIN + Feed + DEFAULT_PARAMS; end; end.
procedure TfrmMainFacebook.Button1Click(Sender: TObject); var fc : TFacebookCollector; begin fc := TFacebookCollector.Create(self, FacebookData, 'gyeonkorea'); fc.GetFacebookData(1, cl.StrToDateTime('2016-01-01'), AccessToken); FacebookData.LoadData(1); Memo2.Lines.Text := FacebookData.ToString; fc.Free; end;
6. FACEBOOK Feed 수집테스트 결과(총 수집건수 212건)
'프로그래밍 > Delphi' 카테고리의 다른 글
DelphiXE 10 Seatle IDE가 느려지는 현상 개선 (0) | 2016.10.06 |
---|---|
[Tips] 등록된 프레임을 갑자기 사용할 수 없을 때 (0) | 2016.09.05 |
[Delphi-Tip]지정된 프로그램이 실행중인지 체크 (1) | 2016.03.17 |
[델파이] 바람직한 주석(Comment)달기 (0) | 2015.10.07 |
투명 PNG 이미지를 투명하지 않은 다른 이미지 포멧으로 저장하기 (0) | 2015.06.03 |