Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

React, Redux and es6/7

784 views

Published on

1. 가볍게 훑어보는 React
2. 대충 보는 Redux
3. 자주 쓰이는 ES6/7 문법
4. 알아두면 좋을 ES6 문법
-
회사 내 마케팅 플랫폼 엔지니어 대상으로 한 COP 발표자료

Published in: Engineering
  • Be the first to comment

React, Redux and es6/7

  1. 1. REACT | REDUX | ES6/7 THE 2ND MPO COP – by Jabi (Cho Dongho)
  2. 2. AGENDA 1. 가볍게 훑어보는 React 2. 대충 보는 Redux 3. 자주 쓰이는 ES6/7 문법 4. 알아두면 좋을 ES6 문법
  3. 3. 가볍게 훑어보는 REACT
  4. 4. React lifecycle
  5. 5. React lifecycle
  6. 6. React lifecycle https://medium.com/@eddychang_86557/react-lifecycle-methods-diagram-38ac92bb6ff1
  7. 7. React ES6 classes https://facebook.github.io/react/docs/reusable-components.html#es6-classes export default class Counter extends React.Component {
 constructor(props) {
 super(props);
 this.state = { count: props.initialCount };
 this.tick = this.tick.bind(this);
 }
 
 tick() {
 this.setState({ count: this.state.count + 1 });
 }
 
 render() {
 return (
 <div onClick={this.tick}>
 Clicks: { this.state.count }
 </div>
 );
 }
 }
 
 Counter.propTypes = {
 initialCount: React.PropTypes.number
 };
 
 Counter.defaultProps = {
 initialCount: 0
 }; export default React.createClass({
 propTypes: {
 initialCount: React.PropTypes.number
 },
 
 getDefaultProps: function() {
 return {
 initialCount: 0
 };
 },
 
 getInitialState: function() {
 return { count: this.props.initialCount };
 },
 
 tick: function() {
 this.setState({ count: this.state.count + 1 });
 },
 
 render: function() {
 return (
 <div onClick={this.tick}>
 Clicks: { this.state.count }
 </div>
 );
 }
 });
  8. 8. React ES6 classes https://facebook.github.io/react/docs/reusable-components.html#es6-classes export default React.createClass({
 propTypes: {
 initialCount: React.PropTypes.number
 },
 
 getDefaultProps: function() {
 return {
 initialCount: 0
 };
 },
 
 getInitialState: function() {
 return { count: this.props.initialCount };
 },
 
 tick: function() {
 this.setState({ count: this.state.count + 1 });
 },
 
 render: function() {
 return (
 <div onClick={this.tick}>
 Clicks: { this.state.count }
 </div>
 );
 }
 }); export default class Counter extends React.Component {
 static propTypes = {
 initialCount: React.PropTypes.number
 };
 
 static defaultProps = {
 initialCount: 0
 };
 
 state = { count: this.props.initialCount };
 
 constructor(props) {
 super(props);
 this.tick = this.tick.bind(this);
 }
 
 tick() {
 this.setState({ count: this.state.count + 1 });
 }
 
 render() {
 return (
 <div onClick={this.tick}>
 Clicks: { this.state.count }
 </div>
 );
 }
 }
  9. 9. React ES6 classes METHOD BINDING RECOMMENED https://facebook.github.io/react/docs/reusable-components.html#no-autobinding
  10. 10. React ES6 classes METHOD BINDING RECOMMENED https://facebook.github.io/react/docs/reusable-components.html#no-autobinding export default class Counter extends Component {
 tick() {
 this.setState({ count: this.state.count + 1 });
 }
 render() {
 return (
 <div onClick={::this.tick} />
 );
 }
 } BE CAREFUL (function bind operator)
  11. 11. React Performance Props vs State - state를가진컴포넌트는복잡도를증가시킴 - 가급적이면최상위컴포넌트를제외하고는props만으로제어 React.PureComponent - react-addons-pure-render-mix구현 - reactv15.3.0부터사용가능 https://facebook.github.io/react/docs/pure-render-mixin.html https://facebook.github.io/react/docs/advanced-performance.html
  12. 12. 대충 보는 REDUX
  13. 13. official: http://redux.js.org official (ko): http://dobbit.github.io/redux cartoon intro to redux: 
 http://bestalign.github.io/2015/10/26/cartoon-intro-to-redux
  14. 14. Redux View Action Dispatcher Store React = Redux = View Controller Model
  15. 15. Redux 데이터의흐름이단방향 애플리케이션의모든데이터(state)는
 하나의모델(store)안에하나의객체트리구조로저장됨 데이터(state)는읽기전용
  16. 16. Redux Action Store Reducer - 상태의변화를위해이뤄져야할행위에대한처리 - repository에서데이터로딩등 - 액션의결과로상태를변경 - 응용프로그램의크기에따라단일또는여러리듀서로관리 - 응용프로그램의전체데이터를관리(dispatch포함)
  17. 17. EXAMPLE - VIEW
  18. 18. EXAMPLE - VIEW SummaryContainer
  19. 19. EXAMPLE - VIEW SummaryContainer SummaryList Pagination
  20. 20. EXAMPLE - VIEW SummaryContainer SummaryList Pagination Summary Summary
  21. 21. EXAMPLE - VIEW SummaryContainer SummaryList Pagination Summary Summary ProgressBar ProgressBar
  22. 22. EXAMPLE - VIEW SummaryContainer SummaryList Pagination Summary Summary ProgressBar ProgressBar Download Download
  23. 23. EXAMPLE - MODEL (APPLICATION STATE) { summaries: [ { id: 10, manageCode: 'baby_offline_coupon_15000', title: '로켓배송기저귀분유물티슈', description: '로켓배송 기저귀,분유,물티슈 카테고리 3만원 이상 구매 시 1만5천원 할인', progress { status: 'COMPLETED', progress: 100 } }, { /* ... */ }, ], paging: { page: 1, size: 2, totalPage: 5 } }
  24. 24. EXAMPLE - REDUX export function fetchSummary(params) {
 return {
 thunk: async(dispatch, getState, { repositories: { summary: repository } }) => {
 const { data: summaries } = await repository.getSummaries(params);
 const action = { type: 'UPDATE_SUMMARY', payload: summaries };
 return dispatch(action);
 }
 };
 } summaryAction.js const initialState = {
 summaries: []
 };
 
 export default function summaryReducer(state = initialState, action) {
 switch (action.type) {
 case 'UPDATE_SUMMARY':
 return {
 ...state,
 summaries: action.payload.contents
 }
 }
 } summaryReducer.js
  25. 25. EXAMPLE - REDUX export function fetchSummary(params) {
 return {
 thunk: async(dispatch, getState, { repositories: { summary: repository } }) => {
 const { data: summaries } = await repository.getSummaries(params);
 const action = { type: 'UPDATE_SUMMARY', payload: summaries };
 return dispatch(action);
 }
 };
 } summaryAction.js const initialState = {
 summaries: []
 };
 
 export default function summaryReducer(state = initialState, action) {
 switch (action.type) {
 case 'UPDATE_SUMMARY':
 return {
 ...state,
 summaries: action.payload.contents
 }
 }
 } summaryReducer.js import { combineReducers, createStore } from 'redux';
 import summaryReducer from './summaryReducer';
 import paginationReducer from './paginationReducer';
 import otherReducer from './otherReducer';
 
 const reducer = combineReducers({
 summary: summaryReducer,
 pagination: paginationReducer,
 other: otherReducer
 });
 
 const store = createStore(reducer); store.js
  26. 26. EXAMPLE - CONTAINER import React, { PureComponent, PropTypes } from 'react';
 import { connect } from 'react-redux';
 import { fetchSummary } from '../actions/summaryAction';
 import SummaryList from ‘../components/SummaryList’;
 import Pagination from ‘../components/Pagination’;
 
 @connect(state => ({
 summaries: state.summary.summaries,
 paging: state.pagination.paging
 }), {
 fetchSummary
 })
 export default class SummaryContainer extends PureComponent {
 static propTypes = {
 summaries: PropTypes.array.isRequired,
 paging: PropTypes.object.isRequired,
 fetchSummary: PropTypes.func.isRequired
 };
 
 render() {
 const { summaries, paging } = this.props;
 return (
 <div>
 <SummaryList summaries={summaries} />
 <Pagination pagination={paging}
 onPageChange={this.props.fetchSummary}
 />
 </div>
 );
 }
 }
 containers/SummaryContainer.js
  27. 27. EXAMPLE - COMPONENT import React, { PureComponent, PropTypes } from 'react';
 import Summary from './Summary';
 
 export default class SummaryList extends PureComponent {
 static propTypes = {
 summaries: PropTypes.array.isRequired,
 fetchSummary: PropTypes.func.isRequired
 }; 
 static defaultProps = { summaries: [] };
 
 render() {
 const { summaries } = this.props;
 return (
 <div>
 { summaries.map(summary => <Summary summary={summary} />) }
 </div> 
 );
 }
 }
 components/SummaryList.js
  28. 28. EXAMPLE - COMPONENT import React, { PureComponent, PropTypes } from 'react';
 import ProgressBar from './ProgressBar';
 import Download from './Download';
 
 export default class Summary extends PureComponent {
 static propTypes = {
 summary: PropTypes.object.isRequired
 }; 
 static defaultProps = {
 summary: {}
 }; 
 render() {
 const { id, manageCode, title, description, progress } = this.props;
 return (
 <div>
 <Text label="id" data={id} />
 <Text label="manageCode" data={manageCode} />
 <Text label="title" data={title} />
 <Text label="description" data={description} />
 <ProgressBar progress={progress} />
 <Download id={id} status={progress.status} />
 </div>
 );
 }
 } components/Summary.js Pagination, ProgressBar, Download 컴포넌트는 생략
  29. 29. 개발 중에 발생했던 실수
  30. 30. 만들려는 형태: * 계층형카테고리구조에서앞서선택한카테고리의하위카테고리를계속해서선택
  31. 31. 잘못된 예 export function fetchCategory(code) {
 return {
 thunk: async(dispatch, getState, { repositories: { category: repository } }) => {
 const { data: categories } = await repository.getCategoriesByCode({code});
 const action = { type: 'UPDATE_CATEGORY', payload: categories };
 return dispatch(action);
 }
 };
 } categoryAction.js const initialState = {
 categories: []
 };
 
 export default function categoryReducer(state = initialState, action) {
 switch (action.type) {
 case 'UPDATE_CATEGORY':
 return {
 ...state,
 categories: action.payload
 }
 }
 } categoryReducer.js [ { code: 1010, name: ‘패션/의류잡화’ }, { code: 1011, name: ‘뷰티’ }, { code: 1012, name: ‘출산/유아동’ }, ] request: { code: 0 }
  32. 32. 실제 나타나는 결과 * 앞선depth의카테고리목록이마지막으로가져온카테고리목록으로대체됨
  33. 33. Why?
  34. 34. Why? reducer에서 카테고리 계층을 표현하지 않고 상태값을 저장하기 때문
  35. 35. REDUCER 개선 categories: [ { code: 1010, name: ‘패션/의류잡화’ }, { code: 1011, name: ‘뷰티’ }, { code: 1012, name: ‘출산/유아동’ }, ] categories: [ [ { code: 1010, name: ‘패션/의류잡화’, selected: false }, { code: 1011, name: ‘뷰티’, selected: false }, { code: 1012, name: ‘출산/유아동’, selected: true }, ], [ { code: 2010, name: ‘임신/출산준비할때’, selected: false }, { code: 2011, name: ‘기저귀갈때’, selected: true }, { code: 2012, name: ‘분유/유아식품’, selected: false }, ], [ /* .. */ ] ]
  36. 36. import { REQUEST_CATEGORIES, RECEIVE_CATEGORIES } from '../actions/categoryAction';
 
 const initialState = {
 categories: []
 };
 
 export default function reducer(state = initialState, { type, request, payload }) {
 switch (type) {
 case REQUEST_CATEGORIES:
 return {
 ...state,
 isFetching: true
 };
 case RECEIVE_CATEGORIES:
 const { depth, code } = request;
 const categories = state.categories.slice(0, depth);
 if (depth > 0) {
 categories[depth - 1] = categories[depth - 1].map(o => ({ 
 code: o.code, 
 name: o.name, 
 selected: `${o.code}` === `${code}` 
 }));
 }
 return {
 ...state,
 categories: [...categories, payload],
 isFetching: false
 };
 default:
 return state;
 }
 }
 categoryReducer.js REDUCER 개선
  37. 37. const prefix = 'CATEGORY/';
 
 export const REQUEST_CATEGORIES = `${prefix}REQUEST_CATEGORIES`;
 export const RECEIVE_CATEGORIES = `${prefix}RECEIVE_CATEGORIES`;
 
 export function fetchCategory(depth = 0, code = 0) {
 return {
 thunk: async(dispatch, getState, { repositories: { category: repository } }) => {
 dispatch(requestCategory(code));
 const having = getState().category.categories;
 if (having.length > depth && !code) {
 const requestDepth = depth > 1 ? depth - 1 : 0;
 const categories = having[requestDepth].map(o => ({ code: o.code, name: o.name }));
 const requestCode = requestDepth > 0 ? having[requestDepth - 1].find(o => o.selected).code : 0;
 return dispatch(receiveCategory({ depth: requestDepth, code: requestCode }, categories));
 }
 const { data: categories } = await repository.getCategories(code);
 return dispatch(receiveCategory({ depth, code }, categories));
 }
 };
 }
 
 const requestCategory = (params) => ({
 type: REQUEST_CATEGORIES
 });
 
 const receiveCategory = (request, categories) => ({
 type: RECEIVE_CATEGORIES,
 request,
 payload: categories
 });
 categoryAction.js REDUCER 개선
  38. 38. 자주 쓰이는 ES6/7 문법
  39. 39. VAR VS LET/CONST * var의문제점
 http://chanlee.github.io/2013/12/10/javascript-variable-scope-and-hoisting var 함수범위변수를선언및초기화 const 읽기전용상수를선언 let 블록범위지역변수를선언및초기화
  40. 40. VAR VS LET/CONST var office = 'coupang';
 var office = 'forward ventures'; // do not error occurred
 console.log(office); // forward ventures
 
 let color = 'blue';
 let color = 'black'; // TypeError
 
 let count = 0;
 count = 1;
 console.log(count); // 1
 
 const apple = 'apple';
 apple = 'samsung'; // error
 
 const comic = { name: 'DragonBall', author: ‘Akira Toriyama' };
 comic = { name: ‘One Piece', author: ‘Oda Eiichiro‘ }; // error
 
 comic.name = ‘One Piece';
 comic.author = ‘Oda Eiichiro';
 
 console.log(comic); // name: One Piece, author: Oda Eiichiro
 es6에서는 var 대신 let을 사용하고, 가급적이면 const를 사용하는 것을 추천
 배열이나 객체의 변경도 막겠다면 immutablejs 등을 사용하는 것도 고려
  41. 41. ARROW FUNCTION // function
 [ 1, 3, 7 ].map(function(value) {
 return value * 2;
 });
 
 // arrow function
 [ 1, 3, 7 ].map(value => value * 2);
 
 // object를 반환하는 arrow function
 const toMap = name => {
 return { name: name };
 };
 
 // object를 반환하는 arrow function 간략 표기
 const toMap = name => ({ name: name }); 
 // compare ‘this’ with arrow function and function 
 const object = {
 f1: function() {
 console.log('f1', this);
 function f1_1() { console.log('f1_1', this) }
 setTimeout(f1_1, 1000);
 setTimeout(() => { console.log('f1_2', this) }, 1000);
 },
 f2: () => {
 console.log('f2', this);
 function f2_1() { console.log('f2_1', this) }
 setTimeout(f2_1, 1000);
 setTimeout(() => { console.log('f2_2', this) }, 1000);
 }
 };
 object.f1(); // Object, Window, Window
 object.f2(); // Window, Window, Window arrowfunction에서의this는arrowfunction이정의된지점의this값과같다
  42. 42. DEFAULT PARAMETERS // 파라미터 기본값이 없는 경우 function increase(number) {
 if (typeof number === 'undefined') return '[ERROR] number is undefined’; return number + 1;
 } console.log(increase()); // [ERROR] number is undefined
 console.log(increase(undefined)); // [ERROR] number is undefined
 console.log(increase(10)); // 11
 
 // 파라미터 기본값을 할당한 경우
 function increase(number = 0) {
 return number + 1;
 } console.log(increase()); // 1
 console.log(increase(undefined)); // 1
 console.log(increase(10)); // 11 OBJECT LITERAL : SHORTHAND // es5
 var x = 1;
 var y = 2;
 var object = {
 x: x, y: y
 };
 
 // es6
 const x = 1;
 const x = 2;
 const object = { x, y };

  43. 43. SPREAD SYNTAX // spread syntax
 
 function myFunction(x, y, z) {
 console.log(`x: ${x}, y: ${y}, z: ${z}`);
 }
 
 const args1 = [ 1, 3, 5 ];
 const args2 = [ 'foo', 'bar' ];
 
 myFunction(...args1); // x: 1, y: 3, z: 5
 myFunction(...args2); // x: foo, y: bar, z: undefuned
 
 
 // rest parameters
 
 function restSyntax(x, y, z, ...rest) {
 console.log(`x: ${x}, y: ${y}, z: ${z}, rest: ${rest}`);
 }
 
 restSyntax(...args1); // x: 1, y: 3, z: 5, rest:
 restSyntax(...args1, ...args2); // x: 1, y: 3, z: 5, rest: foo, bar
 restSyntax(9, 8, 7, 6, 5, 4, 3, 2, 1); // x: 9, y: 8, z: 7, rest: 6, 5, 4, 3, 2, 1
 // assign
 
 const arr1 = [ 1, 2 ];
 const arr2 = [ 4, 5, 6 ];
 const arr3 = [ ...arr1, 3, ...arr2 ];
 
 console.log(arr1); // 1, 2
 console.log(arr2); // 4, 5, 6
 console.log(arr3); // 1, 2, 3, 4, 5, 6
  44. 44. DESTRUCTURING ASSIGNMENT : ARRAY const arr = [ 1, 2, 3, 4, 5, 6 ];
 
 // assign
 const [ first, second ] = arr;
 console.log(first); // 1
 console.log(second); // 2
 
 // assign with pass some value
 const [ first, , , fourth ] = arr;
 console.log(first, fourth); // 1, 4
 
 // assign rest
 const [ first, second, ...rest ] = arr;
 console.log(first, second); // 1, 2
 console.log(rest); // [ 3, 4, 5, 6 ]
 
 // assign with default value
 const [ x, y = 999 ] = [ 1 ];
 console.log(x); // 1
 console.log(y); // 999
 
 // nested array
 const [ a, [ b ]] = [ 1, [ 2 ]];
 console.log(a); // 1
 console.log(b); // 2
 // function
 function myFunction([ x, y, z = 999]) { console.log(`x: ${x}, y: ${y}, z: ${z}`); }
 myFunction([ 1, 2, 3 ]); // x: 1, y: 2, z: 3
 myFunction([ 1, 2 ]); // x: 1, y: 2, z: 999
 myFunction(undefined); // Error
 
 // function with default value
 function myFunctionWithDefault([ x, y, z = 999] = []) { console.log(`x: ${x}, y: ${y}, z: ${z}`); }
 myFunctionWithDefault(undefined); // x: undefined, y: undefined, z: 999
  45. 45. DESTRUCTURING ASSIGNMENT : OBJECT // assign const result = {
 success: false,
 message: ‘exception occurred’,
 callback: () => { console.log('callback function’); }
 }; const { success, message, callback } = result;
 console.log(success); // false
 console.log(message); // 예기치 못한 문제가 발생했습니다.
 console.log(callback()); // callback function
 // nested object const complex = {
 id: 1,
 detail: { name: ‘Jabi', team: 'Santorini' }
 };
 const { detail: { name, team } } = complex;
 console.log(name, team); // Jabi, Santorini
 // assign to other name
 const { detail: { name: userName, team: teamName }} = complex;
 console.log(userName, teamName); // Jabi, Santorini
 // function function myFunction({ id, name, team = 'island' }) {
 console.log(`id: ${id}, name: ${name}, team: ${team}`)
 }
 myFunction({ name: 'Jabi' }); // id: undefined, name: Jabi, team: island
 myFunction(undefined); // Error
 // function with default value
 function myFunctionWithDefault({ id, name, team = 'island' } = {}) {
 console.log(`id: ${id}, name: ${name}, team: ${team}`)
 }
 myFunctionWithDefault(undefined); // // id: undefined, name: undefined, team: island
  46. 46. OBJECT SPREAD / REST SYNTAX (ES7 PROPOSAL STAGE-2) // in react
 
 export default class Sample extends React.Component {
 render() {
 const props = {
 multiple: true,
 readOnly: true,
 size: 3,
 onClick: this.props.onClick
 };
 return (
 <select {...props}>
 <option>option</option>
 </select>
 );
 }
 }
 
 // in redux
 
 function todoApp(state = initialState, action) {
 switch (action.type) {
 case SET_VISIBILITY_FILTER:
 return { ...state, visibilityFilter: action.filter }
 default:
 return state
 }
 }

  47. 47. OBJECT.ASSIGN const o1 = { a: 1 };
 const o2 = { b: 2 };
 const o3 = { b: 20, c: 30 };
 
 const r1 = Object.assign({}, o1, o2);
 const r2 = Object.assign({}, o1, o2, o3);
 const r3 = Object.assign({}, undefined, o2, o3);
 
 console.log(r1); // { a: 1, b: 2 }
 console.log(r2); // { a: 1, b: 20, c: 30 }
 console.log(r3); // { b: 20, c: 30 }
 
 const warning = Object.assign(o1, o2, o3);
 
 console.log(warning); // { a: 1, b: 20, c: 30 }
 console.log(o1); // { a: 1, b: 20, c: 30 } TEMPLATE STRING const name = 'Jabi';
 
 const templateString = `Hello, ${name}. Good to see you.`;
 console.log(templateString); // Hello, Jabi. Good to see you.
 
 const multiline = `first line
 and
 
 last line
 `;
 
 console.log(multiline);
 /*
 fist line
 and
 
 last line
 */
  48. 48. 알아두면 좋을 ES6 문법
  49. 49. 알아두면 좋을 ES6 문법 Promise Set / Map yield / generator 비동기관련하여자주쓰이므로흐름정도는알아두면좋음 new Set([1,2,3]).has(2), new Map({foo:’bar’}).has(‘foo’) javascript에 대해 좀 더 파고 싶다면. ( #MDN / Ben Nadel's blog )
  50. 50. 자주 쓰지만 안본 ES6/7 문법
  51. 51. 자주 쓰지만 안본 ES6/7 문법 뻔한 내용이라…
  52. 52. 자주 쓰지만 안본 ES6/7 문법 classes (es6) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes 뻔한 내용이라…
  53. 53. 자주 쓰지만 안본 ES6/7 문법 classes (es6) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes 뻔한 내용이라… export / import (es6) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
  54. 54. 자주 쓰지만 안본 ES6/7 문법 classes (es6) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes 뻔한 내용이라… export / import (es6) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import 설명하려니 내용도 길고 귀차니즘의 압박
  55. 55. 자주 쓰지만 안본 ES6/7 문법 classes (es6) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes 뻔한 내용이라… export / import (es6) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import 설명하려니 내용도 길고 귀차니즘의 압박 (실은 잘 몰라서)
  56. 56. 자주 쓰지만 안본 ES6/7 문법 async / await (es7) https://jakearchibald.com/2014/es7-async-functions/ classes (es6) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes 뻔한 내용이라… export / import (es6) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import 설명하려니 내용도 길고 귀차니즘의 압박 (실은 잘 몰라서)
  57. 57. THANK YOU

×