foxbpm 시리즈 의 - signavio 사건 원형, OOP 사상 깊이 분석

21382 단어 sign
지난 블 로 그 는 이 시스템 의 메타 데이터 모델 을 간단하게 소개 하 였 으 며, 이번 주 에 도 SIGNAVIO 프로 세 스 디자이너 라 는 오픈 소스 시스템 의 일부 기능 의 핵심 코드 를 계속 소개 하 였 다.
 
이벤트 구동 원형
우선 전체 시스템 의 사건 원형 을 살 펴 보 자.다른 핵심 작업 과 마찬가지 로 이벤트 관련 인터페이스 도 ORYX. Editor 대상 에 봉 인 됩 니 다. 등록 취소, 실행 취소, 이벤트 일시 정지, 이벤트 활성화 등 인 터 페 이 스 를 포함 하고 JS 언어 자체 가 함수 식 프로 그래 밍 에 좋 은 지원 을 하기 때문에 시스템 이벤트 원형 이 쉽게 실 현 됩 니 다.
 
시스템 이벤트 응답 유형:
1. 동작 응답 이벤트, 예 를 들 어 마우스 클릭 이벤트, 키보드 조작 이벤트 등 은 주로 HTML DOCUMENT 에 의 해 이 루어 집 니 다. 디자이너 는 자신의 업무 동태 에 대해 사건 감청 을 추가 하면 됩 니 다.
 
2. 기능 응답 이벤트, 전체 디자이너 시스템 은 이 이벤트 모델 을 바탕 으로 이 루어 집 니 다. 예 를 들 어 도구 모음 명령 실행 이벤트, 노드 요소 드래그 이벤트, 메뉴 이벤트 등 입 니 다.
 
    모든 이벤트 형식의 상수 정 의 는 다음 코드 와 같 습 니 다.
//      、   HTML DOCUMENT  
ORYX.CONFIG.EVENT_MOUSEDOWN =			"mousedown";
ORYX.CONFIG.EVENT_MOUSEUP =				"mouseup";
ORYX.CONFIG.EVENT_MOUSEOVER =			"mouseover";
ORYX.CONFIG.EVENT_MOUSEOUT =			"mouseout";
ORYX.CONFIG.EVENT_MOUSEMOVE =			"mousemove";
ORYX.CONFIG.EVENT_DBLCLICK =			"dblclick";
ORYX.CONFIG.EVENT_KEYDOWN =				"keydown";
ORYX.CONFIG.EVENT_KEYUP =				"keyup";

ORYX.CONFIG.EVENT_LOADED =				"editorloaded";
	
//      
ORYX.CONFIG.EVENT_EXECUTE_COMMANDS =		"executeCommands";
ORYX.CONFIG.EVENT_STENCIL_SET_LOADED =		"stencilSetLoaded";
ORYX.CONFIG.EVENT_SELECTION_CHANGED =		"selectionchanged";
ORYX.CONFIG.EVENT_SHAPEADDED =				"shapeadded";
ORYX.CONFIG.EVENT_SHAPEREMOVED =			"shaperemoved";
ORYX.CONFIG.EVENT_PROPERTY_CHANGED =		"propertyChanged";
ORYX.CONFIG.EVENT_DRAGDROP_START =			"dragdrop.start";
ORYX.CONFIG.EVENT_SHAPE_MENU_CLOSE =		"shape.menu.close";
ORYX.CONFIG.EVENT_DRAGDROP_END =			"dragdrop.end";
ORYX.CONFIG.EVENT_RESIZE_START =			"resize.start";
ORYX.CONFIG.EVENT_RESIZE_END =				"resize.end";
ORYX.CONFIG.EVENT_DRAGDOCKER_DOCKED =		"dragDocker.docked";
ORYX.CONFIG.EVENT_HIGHLIGHT_SHOW =			"highlight.showHighlight";
ORYX.CONFIG.EVENT_HIGHLIGHT_HIDE =			"highlight.hideHighlight";
ORYX.CONFIG.EVENT_LOADING_ENABLE =			"loading.enable";
ORYX.CONFIG.EVENT_LOADING_DISABLE =			"loading.disable";
ORYX.CONFIG.EVENT_LOADING_STATUS =			"loading.status";
ORYX.CONFIG.EVENT_OVERLAY_SHOW =			"overlay.show";
ORYX.CONFIG.EVENT_OVERLAY_HIDE =			"overlay.hide";
ORYX.CONFIG.EVENT_ARRANGEMENT_TOP =			"arrangement.setToTop";
ORYX.CONFIG.EVENT_ARRANGEMENT_BACK =		"arrangement.setToBack";
ORYX.CONFIG.EVENT_ARRANGEMENT_FORWARD =		"arrangement.setForward";
ORYX.CONFIG.EVENT_ARRANGEMENT_BACKWARD =	"arrangement.setBackward";
ORYX.CONFIG.EVENT_PROPWINDOW_PROP_CHANGED =	"propertyWindow.propertyChanged";
ORYX.CONFIG.EVENT_LAYOUT_ROWS =				"layout.rows";
ORYX.CONFIG.EVENT_LAYOUT_BPEL =				"layout.BPEL";
ORYX.CONFIG.EVENT_LAYOUT_BPEL_VERTICAL =    "layout.BPEL.vertical";
ORYX.CONFIG.EVENT_LAYOUT_BPEL_HORIZONTAL =  "layout.BPEL.horizontal";
ORYX.CONFIG.EVENT_LAYOUT_BPEL_SINGLECHILD = "layout.BPEL.singlechild";
ORYX.CONFIG.EVENT_LAYOUT_BPEL_AUTORESIZE =	"layout.BPEL.autoresize";
ORYX.CONFIG.EVENT_AUTOLAYOUT_LAYOUT =		"autolayout.layout";
ORYX.CONFIG.EVENT_UNDO_EXECUTE =			"undo.execute";
ORYX.CONFIG.EVENT_UNDO_ROLLBACK =			"undo.rollback";
ORYX.CONFIG.EVENT_BUTTON_UPDATE =           "toolbar.button.update";
ORYX.CONFIG.EVENT_LAYOUT = 					"layout.dolayout";
ORYX.CONFIG.EVENT_GLOSSARY_LINK_EDIT = 		"glossary.link.edit";
ORYX.CONFIG.EVENT_GLOSSARY_SHOW =			"glossary.show.info";
ORYX.CONFIG.EVENT_GLOSSARY_NEW =			"glossary.show.new";
ORYX.CONFIG.EVENT_DOCKERDRAG = 				"dragTheDocker";

 이상 코드 는 oryx. debug. js 파일 에 있 습 니 다.
 
 
    이벤트 조작 관련 코드 는 다음 과 같다.
disableEvent: function(eventType){
		if(eventType == ORYX.CONFIG.EVENT_KEYDOWN) {
			this._keydownEnabled = false;
		}
		if(eventType == ORYX.CONFIG.EVENT_KEYUP) {
			this._keyupEnabled = false;
		}
		if(this.DOMEventListeners.keys().member(eventType)) {
			var value = this.DOMEventListeners.remove(eventType);
			this.DOMEventListeners['disable_' + eventType] = value;
		}
	},

	enableEvent: function(eventType){
		if(eventType == ORYX.CONFIG.EVENT_KEYDOWN) {
			this._keydownEnabled = true;
		}
		
		if(eventType == ORYX.CONFIG.EVENT_KEYUP) {
			this._keyupEnabled = true;
		}
		
		if(this.DOMEventListeners.keys().member("disable_" + eventType)) {
			var value = this.DOMEventListeners.remove("disable_" + eventType);
			this.DOMEventListeners[eventType] = value;
		}
	},

	/**
	 *  Methods for the PluginFacade
	 */
	registerOnEvent: function(eventType, callback) {
		if(!(this.DOMEventListeners.keys().member(eventType))) {
			this.DOMEventListeners[eventType] = [];
		}

		this.DOMEventListeners[eventType].push(callback);
	},

	unregisterOnEvent: function(eventType, callback) {
		if(this.DOMEventListeners.keys().member(eventType)) {
			this.DOMEventListeners[eventType] = this.DOMEventListeners[eventType].without(callback);
		} else {
			// Event is not supported
			// TODO: Error Handling
		}
	},

  이벤트 실행:
/**
	* Helper method to execute an event immediately. The event is not
	* scheduled in the _eventsQueue. Needed to handle Layout-Callbacks.
	*/
	_executeEventImmediately: function(eventObj) {
		if(this.DOMEventListeners.keys().member(eventObj.event.type)) {
			this.DOMEventListeners[eventObj.event.type].each((function(value) {
				value(eventObj.event, eventObj.arg);		
			}).bind(this));
		}
	},

	_executeEvents: function() {
		this._queueRunning = true;
		while(this._eventsQueue.length > 0) {
			var val = this._eventsQueue.shift();
			this._executeEventImmediately(val);
		}
		this._queueRunning = false;
	},
	
	/**
	 * Leitet die Events an die Editor-Spezifischen Event-Methoden weiter
	 * @param {Object} event Event , welches gefeuert wurde
	 * @param {Object} uiObj Target-UiObj
	 */
	handleEvents: function(event, uiObj) {
		
		ORYX.Log.trace("Dispatching event type %0 on %1", event.type, uiObj);

		switch(event.type) {
			case ORYX.CONFIG.EVENT_MOUSEDOWN:
				this._handleMouseDown(event, uiObj);
				break;
			case ORYX.CONFIG.EVENT_MOUSEMOVE:
				this._handleMouseMove(event, uiObj);
				break;
			case ORYX.CONFIG.EVENT_MOUSEUP:
				this._handleMouseUp(event, uiObj);
				break;
			case ORYX.CONFIG.EVENT_MOUSEOVER:
				this._handleMouseHover(event, uiObj);
				break;
			case ORYX.CONFIG.EVENT_MOUSEOUT:
				this._handleMouseOut(event, uiObj);
				break;
		}
		/* Force execution if necessary. Used while handle Layout-Callbacks. */
		if(event.forceExecution) {
			this._executeEventImmediately({event: event, arg: uiObj});
		} else {
			this._eventsQueue.push({event: event, arg: uiObj});
		}
		
		if(!this._queueRunning) {
			this._executeEvents();
		}
		
		// TODO: Make this return whether no listener returned false.
		// So that, when one considers bubbling undesireable, it won't happen.
		return false;
	},

  이벤트 초기 화:
_initEventListener: function(){

		// Register on Events
		
		document.documentElement.addEventListener(ORYX.CONFIG.EVENT_KEYDOWN, this.catchKeyDownEvents.bind(this), false);
		document.documentElement.addEventListener(ORYX.CONFIG.EVENT_KEYUP, this.catchKeyUpEvents.bind(this), false);

		// Enable Key up and down Event
		this._keydownEnabled = 	true;
		this._keyupEnabled =  	true;

		this.DOMEventListeners[ORYX.CONFIG.EVENT_MOUSEDOWN] = [];
		this.DOMEventListeners[ORYX.CONFIG.EVENT_MOUSEUP] 	= [];
		this.DOMEventListeners[ORYX.CONFIG.EVENT_MOUSEOVER] = [];
		this.DOMEventListeners[ORYX.CONFIG.EVENT_MOUSEOUT] 	= [];
		this.DOMEventListeners[ORYX.CONFIG.EVENT_SELECTION_CHANGED] = [];
		this.DOMEventListeners[ORYX.CONFIG.EVENT_MOUSEMOVE] = [];
				
	},

 
상기 사건 모델 을 바탕 으로 시스템 의 많은 복잡 한 기능 이 순조롭게 실 현 될 수 있다.
예 를 들 어 노드 요소 가 드래그 를 시작 할 때 노드 요소 가 지원 하 는 규칙 메뉴 항목 을 숨 겨 야 합 니 다. 노드 요소 가 드래그 를 종료 할 때 기본적으로 선택 하고 노드 요소 가 지원 하 는 규칙 메뉴 항목 을 보 여 줍 니 다.
이 시스템 을 잘 아 는 독자 들 은 이 기능 을 알 아야 한다.그 핵심 코드 는 다음 과 같다.
 
    이벤트 등록 코드:
ORYX.Plugins.ShapeMenuPlugin = {	
          construct: function(facade) {
		this.facade = facade;
		this.alignGroups = new Hash();
		var containerNode = this.facade.getCanvas().getHTMLContainer();
		this.shapeMenu = new ORYX.Plugins.ShapeMenu(containerNode);
		this.currentShapes = [];

		// Register on dragging and resizing events for show/hide of ShapeMenu
		this.facade.registerOnEvent(ORYX.CONFIG.EVENT_DRAGDROP_START, this.hideShapeMenu.bind(this));
		this.facade.registerOnEvent(ORYX.CONFIG.EVENT_DRAGDROP_END,  this.showShapeMenu.bind(this));

    
   이벤트 방법 관련 코드:
hideShapeMenu: function(event) {
		window.clearTimeout(this.timer);
		this.timer = null;
		this.shapeMenu.hide();
	},

	showShapeMenu: function( dontGenerateNew ) {
		if( !dontGenerateNew || this.resetElements ){
			window.clearTimeout(this.timer);
			this.timer = window.setTimeout(function(){
					// Close all Buttons
				this.shapeMenu.closeAllButtons();
				// Show the Morph Button
				this.showMorphButton(this.currentShapes);
				// Show the Stencil Buttons
				this.showStencilButtons(this.currentShapes);	
				// Show the ShapeMenu
				this.shapeMenu.show(this.currentShapes);
				this.resetElements = false;
			}.bind(this), 300)
			
		} else {
			window.clearTimeout(this.timer);
			this.timer = null;
			// Show the ShapeMenu
			this.shapeMenu.show(this.currentShapes);
		}
	},

 
 
예 를 들 어 선 에 DragDocker 를 추가 할 때 선 에 Docker 가 존재 한다 면 마우스 가 가 는 줄 로 이동 할 때 추 가 된 Docker 를 동적 으로 표시 해 야 합 니 다. 핵심 코드 는 다음 과 같 습 니 다.
 
이벤트 등록 코드:
ORYX.Plugins.DragDocker = Clazz.extend({
	/**
	 *	Constructor
	 *	@param {Object} Facade: The Facade of the Editor
	 */
	construct: function(facade) {
		this.facade = facade;
		// Set the valid and invalid color
		this.VALIDCOLOR 	= ORYX.CONFIG.SELECTION_VALID_COLOR;
		this.INVALIDCOLOR 	= ORYX.CONFIG.SELECTION_INVALID_COLOR;
		// Define Variables 
		this.shapeSelection = undefined;
		this.docker 		= undefined;
		this.dockerParent   = undefined;
		this.dockerSource 	= undefined;
		this.dockerTarget 	= undefined;
		this.lastUIObj 		= undefined;
		this.isStartDocker 	= undefined;
		this.isEndDocker 	= undefined;
		this.undockTreshold	= 10;
		this.initialDockerPosition = undefined;
		this.outerDockerNotMoved = undefined;
		this.isValid 		= false;
		
		// For the Drag and Drop
		// Register on MouseDown-Event on a Docker
		this.facade.registerOnEvent(ORYX.CONFIG.EVENT_MOUSEDOWN, this.handleMouseDown.bind(this));
		this.facade.registerOnEvent(ORYX.CONFIG.EVENT_DOCKERDRAG, this.handleDockerDrag.bind(this));

		
		// Register on over/out to show / hide a docker
		this.facade.registerOnEvent(ORYX.CONFIG.EVENT_MOUSEOVER, this.handleMouseOver.bind(this));
		this.facade.registerOnEvent(ORYX.CONFIG.EVENT_MOUSEOUT, this.handleMouseOut.bind(this));		
		
		
	},

 
  이벤트 방법 구현 코드:
/**
	 * MouseOver Handler
	 *
	 */
	handleMouseOver: function(event, uiObj) {
		// If there is a Docker, show this		
		if(!this.docker && uiObj instanceof ORYX.Core.Controls.Docker) {
			uiObj.show()	
		} else if(!this.docker && uiObj instanceof ORYX.Core.Edge) {
			uiObj.dockers.each(function(docker){
				docker.show();
			})
		}
	},

 
 OOP 사상
          시스템 을 더욱 유연성 있 고 확장 가능 하 게 하기 위해 SIGNAVIO 의 전체적인 구조 도 OOP 사상 으로 프로 그래 밍 을 한다. OOP 관련 지식 과 JS 관련 기초 지식 은 더 이상 군말 하지 않 는 다. (JAVA 언어 와 BS 구조 가 유행 하 는 오늘날 OOP 와 JS 를 모 르 는 프로그래머 는 상상 할 수 없다)클래스 의 계승 은 JS 의 prototype 을 바탕 으로 이 루어 집 니 다.
 
실 현 된 핵심 코드 는 다음 과 같다 (코드 의 영문 주석 포함).
 
/**
 * The super class for all classes in ORYX. Adds some OOP feeling to javascript.
 * See article "Object Oriented Super Class Method Calling with JavaScript" on
 * http://truecode.blogspot.com/2006/08/object-oriented-super-class-method.html
 * for a documentation on this. Fairly good article that points out errors in
 * Douglas Crockford's inheritance and super method calling approach.
 * Worth reading.
 * @class Clazz
 */
var Clazz = function() {};

/**
 * Empty constructor.
 * @methodOf Clazz.prototype
 */
Clazz.prototype.construct = function() {};

/**
 * Can be used to build up inheritances of classes.
 * @example
 * var MyClass = Clazz.extend({
 *   construct: function(myParam){
 *     // Do sth.
 *   }
 * });
 * var MySubClass = MyClass.extend({
 *   construct: function(myParam){
 *     // Use this to call constructor of super class
 *     arguments.callee.$.construct.apply(this, arguments);
 *     // Do sth.
 *   }
 * });
 * @param {Object} def The definition of the new class.
 */
Clazz.extend = function(def) {
    var classDef = function() {
        if (arguments[0] !== Clazz) { this.construct.apply(this, arguments); }
    };
    
    var proto = new this(Clazz);
    var superClass = this.prototype;
    
    for (var n in def) {
        var item = def[n];                        
        if (item instanceof Function) item.$ = superClass;
        proto[n] = item;
    }

    classDef.prototype = proto;
    
    //Give this new class the same static extend method    
    classDef.extend = this.extend;        
    return classDef;
};

 
 
상기 코드 는 계승 기능 을 실현 하 는 템 플 릿 을 제공 하 는 것 과 같 고 상기 템 플 릿 으로 정 의 된 대상 은 모두 계승 기능 을 가진다.
핵심 코드 설명:
1. classDef. extend = this. extend 기능: 이 템 플 릿 으로 대상 을 정의 할 때 이 계승 템 플 릿 도 이 대상 에 게 부여 하여 이 대상 이 하위 대상 을 정의 하 는 능력 을 가지 게 하여 진정한 의미 의 계승 을 실현 합 니 다.
2. this. construct. apply (this, arguments) 와 같은 코드 의 기능 은 이 템 플 릿 으로 대상 을 정의 할 때 해당 대상 의 구조 함수 construct 를 호출 하 는 것 입 니 다.
3、classDef.prototype = proto;속성 을 계승 하 다.
 
꽃 은 물 에서 흘러 내 리 며 템 플 릿 이 정의 되 어 호출 이 쉬 워 집 니 다!
 
시스템 의 이 줄 코드 를 보십시오.
 
 ORYX.Core.UIObject = Clazz.extend(ORYX.Core.UIObject);

 Clazz. extend 는 바로 우리 가 방금 정의 한 계승 템 플 릿 방법 입 니 다.저희 도 템 플 릿 방법 으로 정 의 된 대상 내부 에 construct 구조 함수 가 정의 되 어 있어 야 한다 고 말씀 드 렸 습 니 다. 그래서 검증 을 하고 ORYX. Core. UIObject 원형 정 의 를 추출 해 야 합 니 다.
 
 
    ORYX. Core. UIObject 프로 토 타 입 이 정의 하 는 부분 코드:
 
ORYX.Core.UIObject = {
	/**
	 * Constructor of the UIObject class.
	 */
	construct: function(options) {	
		
		this.isChanged = true;			//Flag, if UIObject has been changed since last update.
		this.isResized = true;
		this.isVisible = true;			//Flag, if UIObject's display attribute is set to 'inherit' or 'none'
		this.isSelectable = false;		//Flag, if UIObject is selectable.
		this.isResizable = false;		//Flag, if UIObject is resizable.
		this.isMovable = false;			//Flag, if UIObject is movable.
		
		this.id = ORYX.Editor.provideId();	//get unique id
		this.parent = undefined;		//parent is defined, if this object is added to another uiObject.
		this.node = undefined;			//this is a reference to the SVG representation, either locally or in DOM.
		this.children = [];				//array for all add uiObjects
		
		this.bounds = new ORYX.Core.Bounds();		//bounds with undefined values

		this._changedCallback = this._changed.bind(this);	//callback reference for calling _changed
		this.bounds.registerCallback(this._changedCallback);	//set callback in bounds
		
		if(options && options.eventHandlerCallback) {
			this.eventHandlerCallback = options.eventHandlerCallback;
		}
	},

 
 
봤 어? construct 나 왔 습 니 다!ORYX. Core. UIObject 는 시스템 이 정의 한 JS 대상 이지 만 마지막 으로 계승 템 플 릿 을 사용 하여 이 루어 집 니 다. 따라서 현재 ORYX. Core. UIObject 대상 은 extend 기능 을 가 진 부모 대상 이 어야 합 니 다. 하위 대상 을 확장 할 수 있 습 니 다.
그럼 과연 그 럴 까요?
우 리 는 시스템 에서 ORYX. Core. AbstractShape 라 는 종 류 를 찾 아 정 의 를 봅 니 다.
 
   ORYX. Core. AbstractShape 부분 정의 코드:
ORYX.Core.AbstractShape = ORYX.Core.UIObject.extend(
/** @lends ORYX.Core.AbstractShape.prototype */
{

	/**
	 * Constructor
	 */
	construct: function(options, stencil) {
		
		arguments.callee.$.construct.apply(this, arguments);
		
		this.resourceId = ORYX.Editor.provideId(); //Id of resource in DOM
		
		// stencil reference
		this._stencil = stencil;
		// if the stencil defines a super stencil that should be used for its instances, set it.
		if (this._stencil._jsonStencil.superId){
			stencilId = this._stencil.id()
			superStencilId = stencilId.substring(0, stencilId.indexOf("#") + 1) + stencil._jsonStencil.superId;
			stencilSet =  this._stencil.stencilSet();
			this._stencil = stencilSet.stencil(superStencilId);
		}
		
		//Hash map for all properties. Only stores the values of the properties.
		this.properties = new Hash();
		this.propertiesChanged = new Hash();

		// List of properties which are not included in the stencilset, 
		// but which gets (de)serialized
		this.hiddenProperties = new Hash();
		
		
		//Initialization of property map and initial value.
		this._stencil.properties().each((function(property) {
			var key = property.prefix() + "-" + property.id();
			this.properties[key] = property.value();
			this.propertiesChanged[key] = true;
		}).bind(this));
		
		// if super stencil was defined, also regard stencil's properties:
		if (stencil._jsonStencil.superId) {
			stencil.properties().each((function(property) {
				var key = property.prefix() + "-" + property.id();
				var value = property.value();
				var oldValue = this.properties[key];
				this.properties[key] = value;
				this.propertiesChanged[key] = true;

				// Raise an event, to show that the property has changed
				// required for plugins like processLink.js
				//window.setTimeout( function(){

					this._delegateEvent({
							type	: ORYX.CONFIG.EVENT_PROPERTY_CHANGED, 
							name	: key, 
							value	: value,
							oldValue: oldValue
						});

				//}.bind(this), 10)

			}).bind(this));
		}

	},

 
수수께끼 가 철저히 밝 혀 졌 으 니 우 리 는 발견 할 수 있다. ORYX. Core. AbstractShape 의 정 의 는 단순 한 JS 문법 '{}' 이 아니 라 ORYX. Core. UIObject 대상 의 extend 방법 을 통 해 알 수 있 습 니 다. ORYX. Core. AbstractShape 의 구조 함수 에 이러한 코드 가 추가 되 었 습 니 다. "arguments. callee. $. construct. apply (this, arguments)" ,똑똑 한 건 분명히 알 아 맞 혔 을 거 야.ORYX. Core. UIObject 는 최종 적 으로 계승 템 플 릿 을 사용 하여 실현 하 는 대상 이기 때문에 extend 기능, 동종 원리, ORYX. Core. AbstractShape 도 extend 기능 을 가지 게 되 었 다. 그러면 전체 시스템 계승 시스템 의 초기 형태 도 나 타 났 다. 바로 SIGNAVIO 가 현재 대상 을 대상 으로 하 는 시스템 구조 이다.이렇게 되면 시스템 의 개발 은 과정 식 함수 식 이 아니 라 시스템 도 더욱 유연성 을 가지 고 확장 성과 유지 가능성 을 가진다.
 
 
 
 
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = [email protected]
 
=====================================================================
 
 
 

좋은 웹페이지 즐겨찾기