노드}
14 | funcValue={() => { console.log('메세지'); }}
15 | />
16 | );
17 | }
18 | }
19 |
20 | export default App;
21 |
--------------------------------------------------------------------------------
/src/07/ActionComponent01.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from '../04/Button';
4 |
5 | class ActionComponent extends PureComponent {
6 | render() {
7 | const { setAge } = this.props;
8 |
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | ActionComponent.propTypes = {
18 | setAge: PropTypes.func,
19 | };
20 |
21 | export default ActionComponent;
22 |
--------------------------------------------------------------------------------
/pages/register.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import AppLayout from '../src/08/components/AppLayout';
3 | import RegisterPageContainer from '../src/08/containers/signup/RegisterPageContainer';
4 | import NotificationContainer from '../src/08/containers/NotificationContainer';
5 |
6 | class RegisterDocument extends PureComponent {
7 | render() {
8 | return (
9 |
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | export default RegisterDocument;
18 |
--------------------------------------------------------------------------------
/src/07/actions/searchFilterActions.js:
--------------------------------------------------------------------------------
1 | export const SET_FILTER = 'searchFilter/SET_FILTER';
2 |
3 | export const setFilter = (filterName, value) => ({
4 | type: SET_FILTER,
5 | payload: {
6 | filterName,
7 | value,
8 | },
9 | });
10 |
11 | export const RESET_FILTER = 'searchFilter/RESET_FILTER';
12 |
13 | export const resetFilter = () => ({
14 | type: RESET_FILTER,
15 | });
16 |
17 | export const setNameFilter = name => setFilter('name', name);
18 | export const setAgeFilter = age => setFilter('age', age);
19 | export const setIdFilter = id => setFilter('id', id);
20 |
--------------------------------------------------------------------------------
/src/07/reducers/searchFilterReducer.js:
--------------------------------------------------------------------------------
1 | import { SET_FILTER, RESET_FILTER } from '../actions/searchFilterActions';
2 |
3 | const initState = {};
4 |
5 | export default function reducer(state = initState, action) {
6 | const { type, payload } = action;
7 |
8 | switch(type) {
9 | case SET_FILTER: {
10 | const { filterName, value } = payload;
11 | return {
12 | ...state,
13 | [filterName]: value,
14 | };
15 | }
16 | case RESET_FILTER: {
17 | return initState;
18 | }
19 | default:
20 | return state;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/03/UnmountLifecycleExampleApp.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LifecycleExample from './03/LifecycleExample';
3 |
4 | class App extends React.Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = { hasDestroyed: false };
8 | }
9 | componentDidMount() {
10 | this.setState({ hasDestroyed: true});
11 | }
12 | render() {
13 | return (
14 |
15 |
16 | {this.state.hasDestroyed? null : }
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | export default App;
24 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "public",
4 | "headers": [
5 | {
6 | "source": "/service-worker.js",
7 | "headers": [{ "key": "Cache-Control", "value": "no-cache" }]
8 | }
9 | ],
10 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
11 | "rewrites": [
12 | {
13 | "source": "/api/**",
14 | "function": "apiserver"
15 | },
16 | {
17 | "source": "**",
18 | "function": "next"
19 | }
20 | ]
21 | },
22 | "functions": {
23 | "source": "functions"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/08/components/Notification.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Toast from '../../doit-ui/Toast';
4 |
5 | class Notification extends PureComponent {
6 | render() {
7 | const { showMessage, message, warning } = this.props;
8 | return showMessage && (
9 |
10 | );
11 | }
12 | }
13 |
14 | Notification.propTypes = {
15 | showMessage: PropTypes.bool,
16 | warning: PropTypes.bool,
17 | message: PropTypes.string,
18 | };
19 |
20 | export default Notification;
21 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | // 아래 머터리얼 스타일 설정을 삭제합니다.
3 | // import './materialize.scss';
4 | import './App.css';
5 |
6 | import InputWithStyle from './04/InputWithStyle';
7 |
8 | class App extends Component {
9 | render() {
10 | return (
11 |
12 |
17 |
머터리얼 CSS
18 |
19 |
20 | );
21 | }
22 | }
23 |
24 | export default App;
25 |
--------------------------------------------------------------------------------
/src/08/containers/main/TransactionSearchFilterContainer.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import TransactionSearchFilter from '../../components/main/TransactionSearchFilter';
3 | // import { requestTransactionList } from '../../actions/transactionActions';
4 | import { requestTransactionList } from '../../actions/transactionPackActions';
5 | import { setFilter } from '../../actions/searchFilterActions';
6 |
7 | const mapStateToProps = state => ({
8 | initValues: state.searchFilter.params,
9 | });
10 |
11 | export default connect(mapStateToProps, { setFilter })(TransactionSearchFilter);
12 |
--------------------------------------------------------------------------------
/src/stories/InputWithStyleStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import Input from '../04/InputWithStyle';
5 |
6 | storiesOf('InputWithStyle', module)
7 | .addWithJSX('기본 설정', () => )
8 | .addWithJSX('label 예제', () => )
9 | .addWithJSX('value 예제', () => )
10 | .addWithJSX('autoFocus 예제', () => )
11 | .addWithJSX('errorMessage 예제', () => );
12 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import { configure } from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16.3';
3 | import * as Aphrodite from 'aphrodite';
4 | import * as AphroditeNoImportant from 'aphrodite/no-important';
5 |
6 | Aphrodite.StyleSheetTestUtils.suppressStyleInjection();
7 | AphroditeNoImportant.StyleSheetTestUtils.suppressStyleInjection();
8 |
9 | configure({ adapter: new Adapter() });
10 |
11 | afterEach(() => {
12 | console.error.mockClear();
13 | });
14 |
15 | beforeEach(() => {
16 | jest.spyOn(console, 'error').mockImplementation((e) => {
17 | throw new Error(e);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure, setAddon } from '@storybook/react';
2 | import interopRequireDefault from 'babel-runtime/helpers/interopRequireDefault';
3 | import JSXAddon from 'storybook-addon-jsx';
4 |
5 | // 04-2 스타일 적용할 때 사용된 파일
6 | import '../src/sass/materialize.scss';
7 | import '../src/doit-ui/app.css';
8 |
9 | function loadStories() {
10 | const context = require.context('../src/stories', true, /Story\.jsx$/);
11 |
12 | context.keys().forEach(srcFile => {
13 | interopRequireDefault(context(srcFile));
14 | });
15 | }
16 |
17 | setAddon(JSXAddon);
18 | configure(loadStories, module);
19 |
--------------------------------------------------------------------------------
/src/doit-ui/TableRow.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class TableRow extends PureComponent {
5 | render() {
6 | const { children, isHeader, baseline } = this.props;
7 |
8 | return (
9 |
10 | {React.Children.map(children, child =>
11 | React.cloneElement(child, { baseline, isHeader })
12 | )}
13 |
14 | );
15 | }
16 | }
17 |
18 | TableRow.propTypes = {
19 | children: PropTypes.node,
20 | baseline: PropTypes.bool,
21 | isHeader: PropTypes.bool
22 | };
23 |
24 | export default TableRow;
25 |
--------------------------------------------------------------------------------
/src/02/02-8.js:
--------------------------------------------------------------------------------
1 | // CommonJS 방식
2 | var module = require('./MyModule');
3 | function Func() {
4 | module();
5 | }
6 | module.exports = new Func();
7 |
8 | // RequireJS 방식
9 | // define(['./MyModule'], function(module) {
10 | // function Func() {
11 | // module();
12 | // }
13 | // return new Func();
14 | // });
15 |
16 | // ES6 방식
17 | // import MyModule from './MyModule';
18 | // import { ModuleName } from './MyModule';
19 | // import { ModuleName as RenamedModuleName } from './MyModule';
20 |
21 | // function Func() {
22 | // MyModule();
23 | // }
24 | export const CONST_VALUE = 0;
25 | export default new Func();
26 |
--------------------------------------------------------------------------------
/src/06/DeleteModalContent.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Consumer } from './ModalContext';
3 | import Button from '../04/Button';
4 | import Text from '../04/Text';
5 |
6 | export default function DeleteModalContent({ id, name }) {
7 | return (
8 |
9 | {({ closeModal }) => (
10 |
11 |
12 |
13 | {name}을 정말로 삭제 하시겠습니까?
14 |
15 |
16 |
17 |
18 |
19 | )}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/02/02-7.js:
--------------------------------------------------------------------------------
1 | // ES5의 예
2 | var x = 0;
3 | var y = 0;
4 |
5 | var obj = { x: x, y: y};
6 |
7 | var randomKeyString = 'other';
8 | var combined = {};
9 | combined['one' + randomKeyString] = 'some value';
10 |
11 | var obj2 = {
12 | methodA: function() { console.log('A'); },
13 | methodB: function() { return 0; },
14 | };
15 |
16 | // ES6의 예
17 | var x = 0;
18 | var y = 0;
19 | var obj = { x, y };
20 |
21 | var randomKeyString = 'other';
22 | var combined = {
23 | ['one' + randomKeyString]: 'some value',
24 | };
25 |
26 | var obj2 = {
27 | x,
28 | methodA() { console.log('A'); },
29 | methodB() { return 0; },
30 | };
31 |
--------------------------------------------------------------------------------
/src/07/containers/SearchResetButtonContainer.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { resetFilter } from '../actions/searchFilterActions';
3 | import Button from '../../04/Button';
4 |
5 | const mapStateToProps = state => {
6 | // 검색 입력 값이 없는 경우 버튼의 disabled를 true로 설정합니다.
7 | const disabled = Object.values(state.searchFilter).reduce(
8 | (result, value) => result && !value,
9 | true,
10 | );
11 |
12 | return {
13 | disabled,
14 | };
15 | };
16 |
17 | const mapDispatchToProps = {
18 | onPress: resetFilter,
19 | };
20 |
21 | export default connect(mapStateToProps, mapDispatchToProps)(Button);
22 |
--------------------------------------------------------------------------------
/src/06/ButtonWithLoadingContext.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from '../04/Button';
4 | import withLoadingContext from './withLoadingContext';
5 |
6 | function ButtonWithLoadingContext({ label, loading, setLoading }) {
7 | return (
8 |
11 | );
12 | }
13 |
14 | ButtonWithLoadingContext.propTypes = {
15 | label: PropTypes.string,
16 | loading: PropTypes.bool,
17 | setLoading: PropTypes.func,
18 | };
19 |
20 | export default withLoadingContext(ButtonWithLoadingContext);
21 |
--------------------------------------------------------------------------------
/src/02/02-10.js:
--------------------------------------------------------------------------------
1 | // ES5의 예제
2 | function work1(onDone) {
3 | setTimeout(() => onDone('작업1 완료!'), 100);
4 | }
5 | function work2(onDone) {
6 | setTimeout(() => onDone('작업1 완료!'), 200);
7 | }
8 | function work3(onDone) {
9 | setTimeout(() => onDone('작업3 완료!'), 300);
10 | }
11 | function urgentWork() {
12 | console.log('긴급 작업');
13 | }
14 | // 실제 비동기 함수를 사용하는 예
15 | work1(function(msg1) {
16 | console.log('done after 100ms:' + msg1);
17 | work2(function(msg2) {
18 | console.log('done after 300ms:' + msg2);
19 | work3(function(msg3) {
20 | console.log('done after 600ms:' + msg3);
21 | });
22 | });
23 | });
24 | urgentWork();
25 |
--------------------------------------------------------------------------------
/src/03/Counter.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class Counter extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | this.state = {
7 | //초기 카운트 값을 프로퍼티에서 전달된 값으로 설정한다
8 | count: props.count,
9 | };
10 | this.increaseCount = this.increaseCount.bind(this);
11 | }
12 | increaseCount() {
13 | this.setState(({ count }) => ({ count: count + 1 }));
14 | }
15 | render() {
16 | return (
17 |
18 | 현재 카운트: {this.state.count}
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | export default Counter;
26 |
--------------------------------------------------------------------------------
/src/08/components/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | // import { Link } from 'react-router-dom';
3 | import Link from 'next/link';
4 |
5 | import Text from '../../doit-ui/Text';
6 | import Spacing from '../../doit-ui/Spacing';
7 |
8 | class NotFound extends PureComponent {
9 | render() {
10 | const { url } = this.props.match || {};
11 | return (
12 |
13 |
14 | {url} 페이지를 찾을 수 없습니다.
15 |
16 | 메인 페이지로 이동
17 |
18 | );
19 | }
20 | }
21 |
22 | export default NotFound;
23 |
--------------------------------------------------------------------------------
/src/07/ReduxApp02.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { createStore } from 'redux';
3 | import { Provider } from 'react-redux';
4 |
5 | class ReduxApp extends PureComponent {
6 | store = createStore(
7 | state => state,
8 | { loading: false, name: '두잇 리액트' },
9 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
10 | );
11 |
12 | componentDidMount() {
13 | this.store.dispatch({
14 | type: 'SET_LOADING',
15 | payload: true,
16 | });
17 | }
18 |
19 | render() {
20 | return 리덕스 예제;
21 | }
22 | }
23 |
24 | export default ReduxApp;
25 |
--------------------------------------------------------------------------------
/src/03/CounterApp.jsx:
--------------------------------------------------------------------------------
1 | // src폴더안에 App.js을 연 다음 기존 내용을 모두 지우고 아래의 내용으로 작성해 보세요
2 | import React from 'react';
3 | import Counter from './03/Counter2';
4 |
5 | class App extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | count: 1,
10 | };
11 | // this 오류를 확인한 후에 아래 주석을 삭제해 주세요.
12 | // this.increateCount = this.increateCount.bind(this);
13 | }
14 | increateCount() {
15 | this.setState(({ count }) => ({ count: count + 1}));
16 | }
17 | render() {
18 | return (
19 |
20 | );
21 | }
22 | }
23 |
24 | export default App;
25 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 40vmin;
8 | }
9 |
10 | .App-header {
11 | background-color: #282c34;
12 | min-height: 100vh;
13 | display: flex;
14 | flex-direction: column;
15 | align-items: center;
16 | justify-content: center;
17 | font-size: calc(10px + 2vmin);
18 | color: white;
19 | }
20 |
21 | .App-link {
22 | color: #61dafb;
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from {
27 | transform: rotate(0deg);
28 | }
29 | to {
30 | transform: rotate(360deg);
31 | }
32 | }
33 |
34 | .title {
35 | font-style: italic;
36 | }
37 |
--------------------------------------------------------------------------------
/src/07/AdvReduxApp03.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Provider } from 'react-redux';
3 | import configureStore from './configureStore';
4 | import { setCollection } from './actions/collectionActions01';
5 |
6 | class AdvReduxApp extends PureComponent {
7 | store = configureStore({ loading: false });
8 |
9 | componentDidMount() {
10 | this.store.dispatch(
11 | setCollection([
12 | { id: 1, name: 'John', age: 20 },
13 | { id: 2, name: 'Park', age: 35 },
14 | ]),
15 | );
16 | }
17 |
18 | render() {
19 | return 리덕스 예제;
20 | }
21 | }
22 | export default AdvReduxApp;
23 |
--------------------------------------------------------------------------------
/src/doit-ui/TableBody.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class TableBody extends PureComponent {
5 | render() {
6 | const { children } = this.props;
7 | const { length } = React.Children.toArray(children);
8 |
9 | return (
10 |
11 | {React.Children.map(children, (child, index) => {
12 | const baseline = index < length - 1 ? true : false;
13 | return React.cloneElement(child, { baseline });
14 | })}
15 |
16 | );
17 | }
18 | }
19 |
20 | TableBody.propTypes = {
21 | children: PropTypes.node,
22 | };
23 |
24 | export default TableBody;
25 |
--------------------------------------------------------------------------------
/src/06/FormSubmitButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { Consumer } from './FormContext';
5 | import Button from '../04/Button';
6 |
7 | class FormSubmitButton extends PureComponent {
8 | render() {
9 | const { children } = this.props;
10 | return (
11 |
12 | {({ submit }) => (
13 |
16 | )}
17 |
18 | );
19 | }
20 | }
21 |
22 | FormSubmitButton.propTypes = {
23 | children: PropTypes.node.isRequired,
24 | };
25 |
26 | export default FormSubmitButton;
27 |
--------------------------------------------------------------------------------
/src/doit-ui/Table.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { withStyles, css, withStylesPropTypes } from './withStyles';
4 |
5 | class Table extends PureComponent {
6 | render() {
7 | const { styles, children } = this.props;
8 | return (
9 |
12 | );
13 | }
14 | }
15 |
16 | Table.propTypes = {
17 | ...withStylesPropTypes,
18 | children: PropTypes.node.isRequired,
19 | };
20 |
21 | export default withStyles(({ color, unit }) => ({
22 | table: {
23 | borderCollapse: 'collapse',
24 | width: '100%',
25 | },
26 | }))(Table);
27 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | // import './index.css';
4 | // import App from './App';
5 | import App from './08/CoinApp';
6 | import './doit-ui/app.css';
7 | // import CoinApp from './03/TodoList';
8 | import * as serviceWorker from './serviceWorker';
9 |
10 | ReactDOM.render(, document.getElementById('root'));
11 | // ReactDOM.render(, document.getElementById('root'));
12 |
13 | // If you want your app to work offline and load faster, you can change
14 | // unregister() to register() below. Note this comes with some pitfalls.
15 | // Learn more about service workers: http://bit.ly/CRA-PWA
16 | serviceWorker.unregister();
17 |
--------------------------------------------------------------------------------
/src/07/PresentationComponent.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class PresentationComponent extends PureComponent {
5 | render() {
6 | const { userName, entity } = this.props;
7 | return (
8 |
9 | 이름: {userName}
10 | 선택된 항목: {entity && `name: ${entity.name}, age: ${entity.age}`}
11 |
12 | );
13 | }
14 | }
15 |
16 | PresentationComponent.propTypes = {
17 | userName: PropTypes.string,
18 | entity: PropTypes.shape({
19 | id: PropTypes.number,
20 | name: PropTypes.string,
21 | age: PropTypes.number,
22 | })
23 | };
24 |
25 | export default PresentationComponent;
26 |
--------------------------------------------------------------------------------
/src/stories/TextStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import Text from '../04/Text';
5 |
6 | storiesOf('Text', module)
7 | .addWithJSX('기본 설정', () => 안녕하세요)
8 | .addWithJSX('large 예제', () => 안녕하세요)
9 | .addWithJSX('xlarge 예제', () => 안녕하세요)
10 | .addWithJSX('small 예제', () => 안녕하세요)
11 | .addWithJSX('xsmall 예제', () => 안녕하세요)
12 | .addWithJSX('primary 예제', () => 안녕하세요)
13 | .addWithJSX('secondary 예제', () => 안녕하세요)
14 | .addWithJSX('primary와 large 함께 쓰는 예제', () => 안녕하세요);
15 |
--------------------------------------------------------------------------------
/src/stories/WithHoCStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import Button from '../04/Button';
5 | import Text from '../04/Text';
6 | import withHoC from '../05/withHoC';
7 |
8 | const ButtonWithHoC = withHoC(Button);
9 | const TextWithHoC = withHoC(Text);
10 |
11 | storiesOf('WithHoC', module)
12 | .addWithJSX('기본 설정', () => (
13 |
14 | 안녕하세요
15 | 안녕하세요
16 |
17 | ))
18 | .addWithJSX('large 예제', () => (
19 |
20 | 안녕하세요
21 | 안녕하세요
22 |
23 | ));
24 |
--------------------------------------------------------------------------------
/src/03/ParentComponentWithConst.jsx:
--------------------------------------------------------------------------------
1 | // src폴더안에 App.js을 연 다음 기존 내용을 모두 지우고 아래의 내용으로 작성해 보세요
2 | import React from 'react';
3 | import ChildComponent from './03/ChildComponent';
4 |
5 | class App extends React.Component {
6 | render() {
7 | // 변수 선언
8 | const array = [1,2,3];
9 | const obj = { name: '제목', age: 30};
10 | const node = 노드
;
11 | const func = () => { console.log('메세지'); };
12 | return (
13 |
21 | );
22 | }
23 | }
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/src/13/AsyncComponent.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import Text from '../doit-ui/Text';
3 |
4 | export default class AsyncComponent extends PureComponent {
5 | componentDidMount() {
6 | const { loader } = this.props;
7 |
8 | loader().then(({ default: Component }) => {
9 | this.Component = Component;
10 | this.forceUpdate();
11 | });
12 | }
13 |
14 | render() {
15 | const { loader, ...otherProps } = this.props;
16 | const Component = this.Component;
17 |
18 | return Component ? (
19 |
20 | ) : (
21 |
22 | 로딩중...
23 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/07/reducers/collectionReducer02.js:
--------------------------------------------------------------------------------
1 | import { SET_COLLECTION } from '../actions/collectionActions01';
2 |
3 | const initState = {
4 | ids: [],
5 | entities: {},
6 | };
7 |
8 | export default (state = initState, action) => {
9 | const { type, payload } = action;
10 |
11 | switch (type) {
12 | case SET_COLLECTION: {
13 | const ids = payload.map(entity => entity['id']);
14 | const entities = payload.reduce(
15 | (finalEntities, entity) => ({
16 | ...finalEntities,
17 | [entity['id']]: entity,
18 | }),
19 | {},
20 | );
21 | return { ...state, ids, entities };
22 | }
23 | default:
24 | return state;
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/src/06/FormConsumerExample.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 |
3 | import Input from '../04/InputWithStyle';
4 | import { Consumer } from './FormContext';
5 |
6 | class FormConsumerExample extends PureComponent {
7 | render() {
8 | const { name, ...otherProps } = this.props;
9 | return (
10 |
11 | {({ values, errors, onChange }) => (
12 |
19 | )}
20 |
21 | );
22 | }
23 | }
24 |
25 | export default FormConsumerExample;
26 |
--------------------------------------------------------------------------------
/src/06/LoadingProviderWithNewContext.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const { Provider, Consumer } = React.createContext({});
4 |
5 | export { Consumer };
6 |
7 | export default class LoadingProvider extends React.Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.state = {};
12 | this.setLoading = this.setLoading.bind(this);
13 | }
14 |
15 | setLoading(key, value) {
16 | const newState = { [key]: value };
17 | this.setState(newState);
18 | }
19 |
20 | render() {
21 | const context = {
22 | ...this.state,
23 | setLoading: this.setLoading,
24 | };
25 |
26 | return {this.props.children};
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/07/AdvReduxApp02.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Provider } from 'react-redux';
3 | import configureStore from './configureStore';
4 | import { setLoading, resetLoading } from './actions/loadingActions';
5 | import { setUser } from './actions/userActions';
6 |
7 | class AdvReduxApp extends PureComponent {
8 | store = configureStore({ loading: false });
9 |
10 | componentDidMount() {
11 | this.store.dispatch(setLoading(true));
12 | this.store.dispatch(resetLoading());
13 | this.store.dispatch(setUser({ name: 'Park', age: 20 }));
14 | }
15 |
16 | render() {
17 | return 리덕스 예제;
18 | }
19 | }
20 | export default AdvReduxApp;
21 |
--------------------------------------------------------------------------------
/src/stories/InputStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { action } from '@storybook/addon-actions';
4 |
5 | import Input from '../03/Input';
6 |
7 | storiesOf('Input', module)
8 | .addWithJSX('기본 설정', () => )
9 | .addWithJSX('label 예제', () => )
10 | .addWithJSX('onChange 예제', () => )
11 | .addWithJSX('value 예제', () => )
12 | .addWithJSX('errorMessage 예제', () => )
13 | .addWithJSX('autoFocus 예제', () => );
14 |
--------------------------------------------------------------------------------
/src/07/AdvReduxApp01.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Provider } from 'react-redux';
3 | import configureStore from './configureStore';
4 |
5 | class AdvReduxApp extends PureComponent {
6 | store = configureStore({ loading: false });
7 |
8 | componentDidMount() {
9 | this.store.dispatch({
10 | type: 'SET_LOADING',
11 | payload: true,
12 | });
13 | this.store.dispatch({
14 | type: 'RESET_LOADING',
15 | });
16 | this.store.dispatch({
17 | type: 'SET_USER',
18 | payload: { name: 'Park', age: 20 },
19 | });
20 | }
21 |
22 | render() {
23 | return 리덕스 예제;
24 | }
25 | }
26 | export default AdvReduxApp;
27 |
--------------------------------------------------------------------------------
/src/stories/DoitUIInputStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { action } from '@storybook/addon-actions';
4 |
5 | import Input from '../doit-ui/Input';
6 |
7 | storiesOf('Doit-UI/Input', module)
8 | .addWithJSX('기본 설정', () => )
9 | .addWithJSX('label 예제', () => )
10 | .addWithJSX('onChange 예제', () => )
11 | .addWithJSX('value 예제', () => )
12 | .addWithJSX('errorMessage 예제', () => )
13 | .addWithJSX('autoFocus 예제', () => );
14 |
--------------------------------------------------------------------------------
/src/03/ChildComponent2.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class ChildComponent2 extends React.Component {
5 | render() {
6 | const {
7 | objValue,
8 | requiredStringValue,
9 | } = this.props;
10 |
11 | return (
12 |
13 |
객체값: {String(Object.entries(objValue))}
14 |
필수값: {requiredStringValue}
15 |
16 | );
17 | }
18 | }
19 |
20 | ChildComponent2.propTypes = {
21 | // 객체형 프로퍼티
22 | objValue: PropTypes.shape({
23 | name: PropTypes.string,
24 | age: PropTypes.number,
25 | }),
26 | // 필수 프로퍼티
27 | requiredStringValue: PropTypes.string.isRequired,
28 | }
29 |
30 | export default ChildComponent2;
31 |
--------------------------------------------------------------------------------
/src/05/compose.jsx:
--------------------------------------------------------------------------------
1 | import compose from 'recompose/compose';
2 | import withLoading from './withLoading';
3 | import withState from 'recompose/withState';
4 | const withLoadData = withState('isLoading', 'setIsLoading', true);
5 |
6 | function Component() {
7 | return '완료(컴포넌트 출력)';
8 | }
9 | const ComponentWithLoading = withLoading('로딩중')(Component);
10 | const ComponentWithLoadData = withLoadData(Component);
11 |
12 | const withLoadingAndLoadData = compose(withLoadData, withLoading('로딩중'));
13 | // 조합이 올바르지 못한 예: compose(withLoadData, withLoading)
14 | // 올바르지 못한 예: compose(withLoading('로딩중'), withLoadData)
15 | export const ComponentWithBoth = withLoadingAndLoadData(Component);
16 | // 혹은 compose(withLoadData, withLoading('로딩중'))(Component);
17 |
--------------------------------------------------------------------------------
/src/08/components/main/CoinOverview.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 |
3 | import Heading from '../../../doit-ui/Heading';
4 | import InlineList from '../../../doit-ui/InlineList';
5 |
6 | import CoinDashlet from './CoinDashlet';
7 |
8 | class CoinOverview extends PureComponent {
9 | render() {
10 | return (
11 |
12 | 코인 동향
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | export default CoinOverview;
24 |
--------------------------------------------------------------------------------
/src/sass/components/_pulse.scss:
--------------------------------------------------------------------------------
1 | .pulse {
2 | &::before {
3 | content: '';
4 | display: block;
5 | position: absolute;
6 | width: 100%;
7 | height: 100%;
8 | top: 0;
9 | left: 0;
10 | background-color: inherit;
11 | border-radius: inherit;
12 | transition: opacity .3s, transform .3s;
13 | animation: pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;
14 | z-index: -1;
15 | }
16 |
17 | overflow: visible;
18 | position: relative;
19 | }
20 |
21 | @keyframes pulse-animation {
22 | 0% {
23 | opacity: 1;
24 | transform: scale(1);
25 | }
26 | 50% {
27 | opacity: 0;
28 | transform: scale(1.5);
29 | }
30 | 100% {
31 | opacity: 0;
32 | transform: scale(1.5);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/08/actions/transactionApiActions.js:
--------------------------------------------------------------------------------
1 | import createActions from '../../11/api-redux-pack/createActions';
2 |
3 | const { collection, create } = createActions('transactions');
4 | const PAGE_SIZE = 10;
5 |
6 | export function requestTransactionList(params, _page = 1) {
7 | const meta = {
8 | pageNumber: _page,
9 | pageSize: PAGE_SIZE,
10 | notification: {
11 | success: '거래 목록을 최신 정보로 업데이트하였습니다.',
12 | error: '거래 목록을 갱신하는 중에 문제가 발생하였습니다.',
13 | },
14 | };
15 | return collection(
16 | {
17 | ...params,
18 | _page,
19 | _limit: PAGE_SIZE,
20 | },
21 | meta,
22 | );
23 | }
24 |
25 | export function createTransaction(data, closeModal) {
26 | return create(data, {}, { onSuccess: closeModal });
27 | }
28 |
--------------------------------------------------------------------------------
/src/sass/components/_table_of_contents.scss:
--------------------------------------------------------------------------------
1 | /***************
2 | Nav List
3 | ***************/
4 | .table-of-contents {
5 | &.fixed {
6 | position: fixed;
7 | }
8 |
9 | li {
10 | padding: 2px 0;
11 | }
12 | a {
13 | display: inline-block;
14 | font-weight: 300;
15 | color: #757575;
16 | padding-left: 16px;
17 | height: 1.5rem;
18 | line-height: 1.5rem;
19 | letter-spacing: .4;
20 | display: inline-block;
21 |
22 | &:hover {
23 | color: lighten(#757575, 20%);
24 | padding-left: 15px;
25 | border-left: 1px solid $primary-color;
26 | }
27 | &.active {
28 | font-weight: 500;
29 | padding-left: 14px;
30 | border-left: 2px solid $primary-color;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/sass/components/_tooltip.scss:
--------------------------------------------------------------------------------
1 | .material-tooltip {
2 | padding: 10px 8px;
3 | font-size: 1rem;
4 | z-index: 2000;
5 | background-color: transparent;
6 | border-radius: 2px;
7 | color: #fff;
8 | min-height: 36px;
9 | line-height: 120%;
10 | opacity: 0;
11 | position: absolute;
12 | text-align: center;
13 | max-width: calc(100% - 4px);
14 | overflow: hidden;
15 | left: 0;
16 | top: 0;
17 | pointer-events: none;
18 | visibility: hidden;
19 | background-color: #323232;
20 | }
21 |
22 | .backdrop {
23 | position: absolute;
24 | opacity: 0;
25 | height: 7px;
26 | width: 14px;
27 | border-radius: 0 0 50% 50%;
28 | background-color: #323232;
29 | z-index: -1;
30 | transform-origin: 50% 0%;
31 | visibility: hidden;
32 | }
33 |
--------------------------------------------------------------------------------
/src/06/LoadingProvider.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class LoadingProvider extends React.Component {
5 | constructor(props) {
6 | super(props);
7 |
8 | this.state = { loading: false };
9 | this.setLoading = this.setLoading.bind(this);
10 | }
11 |
12 | getChildContext() {
13 | return {
14 | loading: this.state.loading,
15 | setLoading: this.setLoading,
16 | };
17 | }
18 |
19 | setLoading(loading) {
20 | this.setState({ loading });
21 | }
22 |
23 | render() {
24 | return this.props.children;
25 | }
26 | }
27 |
28 | LoadingProvider.childContextTypes = {
29 | loading: PropTypes.bool,
30 | setLoading: PropTypes.func,
31 | };
32 |
33 | export default LoadingProvider;
34 |
--------------------------------------------------------------------------------
/src/06/ButtonWithLoadingContextAndKey.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from '../04/Button';
4 | import withLoadingContextAndKey, { loadingPropTypes } from './withLoadingContextAndKey';
5 |
6 | function ButtonWithLoadingContext({ children, loading, setLoading }) {
7 | return ;
8 | }
9 |
10 | ButtonWithLoadingContext.propTypes = {
11 | ...loadingPropTypes,
12 | children: PropTypes.string,
13 | };
14 |
15 | export const ButtonWithDefaultLoadingContext = withLoadingContextAndKey('defaultLoadingKey')(
16 | ButtonWithLoadingContext,
17 | );
18 | export const ButtonWithLoading2Context = withLoadingContextAndKey('loading2')(
19 | ButtonWithLoadingContext,
20 | );
21 |
--------------------------------------------------------------------------------
/src/06/withLoadingContext.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export const loadingPropTypes = {
5 | loading: PropTypes.bool,
6 | setLoading: PropTypes.func,
7 | };
8 |
9 | export default WrappedComponent => {
10 | const { displayName, name: componentName } = WrappedComponent;
11 | const wrappedComponentName = displayName || componentName;
12 |
13 | function WithLoadingContext(props, context) {
14 | const { loading, setLoading } = context
15 | return (
16 |
17 | );
18 | };
19 | WithLoadingContext.displayName = `withLoadingContext(${wrappedComponentName})`;
20 | WithLoadingContext.contextTypes = loadingPropTypes;
21 | return WithLoadingContext;
22 | };
23 |
--------------------------------------------------------------------------------
/src/05/lifecycle.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import lifecycle from 'recompose/lifecycle';
3 | import compose from 'recompose/compose';
4 | import withLoading from './withLoading';
5 |
6 | function Page({ content }) {
7 | return (
8 |
9 | 페이지 로딩이 완료되었습니다.
10 | {content}
11 |
12 | );
13 | }
14 |
15 | export const withLoadData = lifecycle({
16 | state: { isLoading: true, content: '' },
17 | componentDidMount: function() {
18 | if (this.props.loadData) {
19 | this.props.loadData().then(content => this.setState({ isLoading: false, content }));
20 | }
21 | },
22 | });
23 |
24 | export const PageWithLoadData = withLoadData(Page);
25 | export const PageWithLoadDataAndLoading = compose(
26 | withLoadData,
27 | withLoading('서버 요청중'),
28 | )(Page);
29 |
--------------------------------------------------------------------------------
/src/01/RCFC.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class RCFC extends Component {
5 | constructor(props) {
6 | super(props);
7 |
8 | }
9 |
10 | componentWillMount() {
11 |
12 | }
13 |
14 | componentDidMount() {
15 |
16 | }
17 |
18 | componentWillReceiveProps(nextProps) {
19 |
20 | }
21 |
22 | shouldComponentUpdate(nextProps, nextState) {
23 |
24 | }
25 |
26 | componentWillUpdate(nextProps, nextState) {
27 |
28 | }
29 |
30 | componentDidUpdate(prevProps, prevState) {
31 |
32 | }
33 |
34 | componentWillUnmount() {
35 |
36 | }
37 |
38 | render() {
39 | return (
40 |
41 |
42 |
43 | );
44 | }
45 | }
46 |
47 | RCFC.propTypes = {
48 |
49 | };
50 |
51 | export default RCFC;
52 |
--------------------------------------------------------------------------------
/src/07/containers/SearchResultTableContainer.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import SearchResultTable from '../SearchResultTable';
3 |
4 | const mapStateToProps = state => {
5 | const { collection, searchFilter } = state;
6 | const hasFilter = Object.values(searchFilter).reduce((result, value) => result || Boolean(value), false);
7 | const { ids, entities } = collection;
8 | const items = ids
9 | .map(id => entities[id])
10 | .filter(
11 | entity =>
12 | !hasFilter ||
13 | Object.entries(searchFilter).reduce(
14 | (result, [key, value]) => result && (!value || `${entity[key]}` === `${value}`),
15 | true,
16 | ),
17 | );
18 |
19 | return { items };
20 | };
21 |
22 | export default connect(mapStateToProps)(SearchResultTable);
23 |
--------------------------------------------------------------------------------
/src/08/middlewares/routerEffects.js:
--------------------------------------------------------------------------------
1 | import { SET_LOCATION } from '../actions/routerActions';
2 | import { setFilter } from '../actions/searchFilterActions';
3 |
4 | function parse(qs) {
5 | const queryString = qs.substr(1);
6 | const chunks = queryString.split('&');
7 | return chunks
8 | .map((chunk) => chunk.split('='))
9 | .reduce((result, [ key, value ]) => ({
10 | ...result,
11 | [key]: value,
12 | }), {});
13 | }
14 |
15 | export default store => nextRunner => action => {
16 | const { type, payload } = action;
17 | const result = nextRunner(action);
18 | if (type === SET_LOCATION) {
19 | const { pathname, search } = payload.location;
20 | if (pathname === '/') {
21 | store.dispatch(setFilter(parse(search)));
22 | }
23 | }
24 | return result;
25 | }
26 |
--------------------------------------------------------------------------------
/src/stories/DoitUICardStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import Text from '../doit-ui/Text';
5 | import Card from '../doit-ui/Card';
6 | import Spacing from '../doit-ui/Spacing';
7 |
8 | storiesOf('Doit-UI/Card', module)
9 | .addWithJSX('Card 예제', () => (
10 |
11 |
12 | 제목
13 |
14 | 내용이 함께 들어갑니다
15 |
16 |
17 | ))
18 | .addWithJSX('Card Spacing 예제', () => (
19 |
20 |
21 | 제목
22 |
23 | 내용이 함께 들어갑니다
24 |
25 |
26 | ));
27 |
--------------------------------------------------------------------------------
/src/stories/WithLoadingStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import Button from '../04/Button';
5 | import Text from '../04/Text';
6 | import withLoading from '../05/withLoading';
7 |
8 | const ButtonWithLoading = withLoading()(Button);
9 | const TextWithLoading = withLoading('로딩중')(Text);
10 |
11 | storiesOf('WithLoading', module)
12 | .addWithJSX('기본 설정', () => (
13 |
14 | 안녕하세요
15 | 안녕하세요
16 |
17 | ))
18 | .addWithJSX('isLoading 예제', () => (
19 |
20 | 안녕하세요
21 | 안녕하세요
22 |
23 | ));
24 |
--------------------------------------------------------------------------------
/src/03/CounterExample.jsx:
--------------------------------------------------------------------------------
1 | // src폴더안에 App.js을 연 다음 기존 내용을 모두 지우고 아래의 내용으로 작성해 보세요
2 | import React from 'react';
3 | import Counter from './03/Counter';
4 | import NewCounter from './03/NewCounter';
5 |
6 | class App extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = { count: 10 };
10 | this.resetCount = this.resetCount.bind(this);
11 | }
12 | resetCount() {
13 | this.setState(({ count }) => ({ count: count + 10 }));
14 | }
15 | render() {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 | }
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/src/08/reducers/notificationReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | SHOW_NOTIFICATION,
3 | HIDE_NOTIFICATION,
4 | } from '../actions/notificationActions';
5 |
6 | const initState = {
7 | message: '',
8 | warning: false,
9 | showMessage: false,
10 | };
11 |
12 | export default (state = initState, action) => {
13 | const { type, payload } = action;
14 |
15 | switch (type) {
16 | case SHOW_NOTIFICATION: {
17 | const { message, warning } = payload;
18 | return {
19 | ...state,
20 | showMessage: true,
21 | message,
22 | warning,
23 | };
24 | }
25 | case HIDE_NOTIFICATION: {
26 | return {
27 | ...state,
28 | message: '',
29 | showMessage: false,
30 | };
31 | }
32 | default:
33 | return state;
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/src/03/OrderForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Input from './Input';
3 |
4 | class OrderForm extends React.PureComponent {
5 | constructor(props) {
6 | super(props);
7 | this.state = {};
8 | this.setValue = this.setValue.bind(this);
9 | }
10 | setValue(name, value) {
11 | this.setState({ [name]: value });
12 | }
13 | render() {
14 | return (
15 |
21 | );
22 | }
23 | }
24 | export default OrderForm;
25 |
--------------------------------------------------------------------------------
/src/08/components/AppLayout.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { withStyles, css, withStylesPropTypes } from '../../doit-ui/withStyles';
4 | import AppNav, { HEIGHT } from './AppNav';
5 |
6 | class AppLayout extends PureComponent {
7 | render() {
8 | const { children, styles } = this.props;
9 | return (
10 |
11 |
12 |
{children}
13 |
14 | );
15 | }
16 | }
17 |
18 | AppLayout.propTypes = {
19 | ...withStylesPropTypes,
20 | children: PropTypes.node,
21 | };
22 |
23 | export default withStyles(({ unit }) => ({
24 | wrapper: {
25 | marginTop: HEIGHT,
26 | },
27 | body: {
28 | padding: unit * 4,
29 | },
30 | }))(AppLayout);
31 |
--------------------------------------------------------------------------------
/src/07/AdvReduxApp08.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Provider } from 'react-redux';
3 | import configureStore from './configureStore';
4 |
5 | import ContainerComponent from './containers/ContainerComponent';
6 | import PresentationComponent from './PresentationComponent';
7 | import DispatchContainer03 from './containers/DispatchContainer03';
8 |
9 | class AdvReduxApp extends PureComponent {
10 | store = configureStore({ loading: false });
11 |
12 | render() {
13 | return (
14 |
15 | 화면 컴포넌트:
16 |
17 | 데이터 컴포넌트:
18 |
19 | 최종 액션 데이터 컴포넌트:
20 |
21 | );
22 | }
23 | }
24 | export default AdvReduxApp;
25 |
--------------------------------------------------------------------------------
/src/03/Counter3.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class Counter3 extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | this.state = {
7 | count: 0,
8 | };
9 | this.increateCount = this.increateCount.bind(this);
10 | this.resetCount = this.resetCount.bind(this);
11 | }
12 | increateCount() {
13 | this.setState(({ count }) => ({ count: count + 1}));
14 | }
15 | resetCount() {
16 | this.setState({ count: 0 });
17 | }
18 | render() {
19 | return (
20 |
21 | 현재 카운트: {this.state.count}
22 |
28 | 버튼 밖으로 커서가 움직이면 0으로 초기화 됩니다.
29 |
30 | );
31 | }
32 | }
33 |
34 | export default Counter3;
35 |
--------------------------------------------------------------------------------
/src/08/containers/RouterStateContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { setLocation } from '../actions/routerActions';
5 | import { withRouter } from 'react-router';
6 | import compose from 'recompose/compose';
7 |
8 | class RouterState extends React.PureComponent {
9 | componentDidMount() {
10 | const { setLocation, location } = this.props;
11 | setLocation(location);
12 | }
13 | componentDidUpdate() {
14 | const { setLocation, location } = this.props;
15 | setLocation(location);
16 | }
17 | render() {
18 | return null;
19 | }
20 | }
21 |
22 | RouterState.propTypes = {
23 | setLocation: PropTypes.func,
24 | location: PropTypes.object,
25 | };
26 |
27 | export default compose(connect(null, { setLocation }), withRouter)(RouterState);
28 |
--------------------------------------------------------------------------------
/src/06/HomePageWithProvider.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import LoadingProvider from './LoadingProvider';
3 | import ButtonWithLoadingContext from './ButtonWithLoadingContext';
4 | import Button from '../04/Button';
5 |
6 | function RowBComponent() {
7 | return ;
8 | }
9 |
10 | function RowCComponent() {
11 | return ;
12 | }
13 |
14 | function TableComponent() {
15 | return (
16 |
20 | );
21 | }
22 | class HomePageComponent extends PureComponent {
23 | render() {
24 | return (
25 |
26 |
27 |
28 |
29 | );
30 | }
31 | }
32 |
33 | export default HomePageComponent;
34 |
--------------------------------------------------------------------------------
/src/06/withLoadingContextAndKey.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DEFAULT_KEY, contextPropTypes } from './LoadingProviderWithKey';
3 |
4 | export const loadingPropTypes = contextPropTypes;
5 |
6 | export default (contextKey = DEFAULT_KEY) => WrappedComponent => {
7 | const { displayName, name: componentName } = WrappedComponent;
8 | const wrappedComponentName = displayName || componentName;
9 |
10 | function WithLoadingContext(props, context) {
11 | const { loading, setLoading } = context[contextKey];
12 | console.log('c', context);
13 | return ;
14 | }
15 | WithLoadingContext.displayName = `withLoadingContext(${wrappedComponentName})`;
16 | WithLoadingContext.contextTypes = {
17 | [contextKey]: contextPropTypes,
18 | };
19 | return WithLoadingContext;
20 | };
21 |
--------------------------------------------------------------------------------
/src/02/02-7-2.js:
--------------------------------------------------------------------------------
1 | // ES5 예제
2 | var list = [0,1];
3 | var item1 = list[0];
4 | var item2 = list[1];
5 | var item3 = list[2] || -1;
6 |
7 | var temp = item2;
8 | item2= item1;
9 | item1 = temp;
10 |
11 | var obj = {
12 | key1: 'one',
13 | key2: 'two',
14 | };
15 |
16 | var key1 = obj.key1;
17 | var key2 = obj.key2;
18 | var key3 = obj.key3 || 'default key3 value';
19 | var newKey1 = key1;
20 |
21 | // ES6 예제
22 | var list = [0, 1];
23 | var [
24 | item1,
25 | item2,
26 | item3 = -1,
27 | ] = list;
28 | [item2, item1] = [item1, item2];
29 |
30 | var obj = {
31 | key1: 'one',
32 | key2: 'two',
33 | };
34 | var {
35 | key1: newKey1,
36 | key2,
37 | key3 = 'default key3 value',
38 | } = obj;
39 |
40 | var [item1, ...otherItems] = [0, 1, 2];
41 | var { key1, ...others } = { key1: 'one', key2: 'two' };
42 | // otherItems = [1, 2]
43 | // others = { key2: 'two' }
44 |
--------------------------------------------------------------------------------
/src/07/AdvReduxApp04.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Provider } from 'react-redux';
3 | import configureStore from './configureStore';
4 | import { setCollection } from './actions/collectionActions01';
5 |
6 | class AdvReduxApp extends PureComponent {
7 | store = configureStore({ loading: false });
8 |
9 | componentDidMount() {
10 | this.store.dispatch(
11 | setCollection([
12 | { id: 1, name: 'John', age: 20 },
13 | { id: 2, name: 'Park', age: 35 },
14 | ]),
15 | );
16 | const { collection } = this.store.getState();
17 | const { ids, entities } = collection;
18 | const originalPayload = ids.map(id => entities[id]);
19 | console.log(originalPayload);
20 | }
21 |
22 | render() {
23 | return 리덕스 예제;
24 | }
25 | }
26 | export default AdvReduxApp;
27 |
--------------------------------------------------------------------------------
/src/06/ButtonWithModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import Button from '../04/Button';
3 | import Text from '../04/Text';
4 | import Modal from './Modal';
5 |
6 | class ButtonWithModal extends PureComponent {
7 | constructor(props) {
8 | super(props);
9 | this.state = { showModal: false };
10 | }
11 | render() {
12 | return (
13 |
14 |
15 | {this.state.showModal && (
16 |
17 |
18 | 정말로 삭제 하시겠습니까?
19 |
20 |
21 |
22 |
23 | )}
24 |
25 | );
26 | }
27 | }
28 |
29 | export default ButtonWithModal;
30 |
--------------------------------------------------------------------------------
/src/stories/FormStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { action } from '@storybook/addon-actions';
4 |
5 | import Form from '../06/Form';
6 | import FormConsumerExample from '../06/FormConsumerExample';
7 | import FormSubmitButton from '../06/FormSubmitButton';
8 |
9 | const validate = ({ name, age }) => {
10 | let errors = {};
11 | if (!name) errors['name'] = '이름을 입력해야합니다.';
12 | if (age && age < 18) errors['age'] = '나이는 18세 이상이여야 합니다.';
13 | return errors;
14 | };
15 |
16 | storiesOf('Form', module).addWithJSX('유효성 검사', () => (
17 |
23 | ));
24 |
--------------------------------------------------------------------------------
/src/sass/components/_color-classes.scss:
--------------------------------------------------------------------------------
1 | // Color Classes
2 |
3 | @each $color_name, $color in $colors {
4 | @each $color_type, $color_value in $color {
5 | @if $color_type == "base" {
6 | .#{$color_name} {
7 | background-color: $color_value !important;
8 | }
9 | .#{$color_name}-text {
10 | color: $color_value !important;
11 | }
12 | }
13 | @else if $color_name != "shades" {
14 | .#{$color_name}.#{$color_type} {
15 | background-color: $color_value !important;
16 | }
17 | .#{$color_name}-text.text-#{$color_type} {
18 | color: $color_value !important;
19 | }
20 | }
21 | }
22 | }
23 |
24 | // Shade classes
25 | @each $color, $color_value in $shades {
26 | .#{$color} {
27 | background-color: $color_value !important;
28 | }
29 | .#{$color}-text {
30 | color: $color_value !important;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/03/DefaultPropsCompoent.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class DefaultPropsComponent extends React.Component {
5 | render() {
6 | let message1 = '';
7 | if (this.props.boolValue === false) {
8 | message1 = 'boolValue 기본값이 false입니다';
9 | }
10 | let message2 = '';
11 | if (this.props.boolValueWithoutDefault === false) {
12 | message2 = 'boolValueWithoutDefault 기본값이 false입니다';
13 | }
14 | return (
15 |
16 | {message1}
17 | {message2}
18 |
19 | );
20 | }
21 | }
22 |
23 | DefaultPropsComponent.propTypes = {
24 | boolValue: PropTypes.bool,
25 | boolValueWithoutDefault: PropTypes.bool,
26 | };
27 |
28 | // 기본값을 선언하는 예제
29 | DefaultPropsComponent.defaultProps = {
30 | boolValue: false,
31 | };
32 |
33 | export default DefaultPropsComponent;
34 |
--------------------------------------------------------------------------------
/src/03/ForceUpdateExample.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class ForceUpdateExample extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | // 상태 정의
7 | this.loading = true;
8 | this.formData = 'no data';
9 | // 이후 콜백 함수를 다룰때 bind를 선언하는 부분에 대해 다룹니다
10 | this.handleData = this.handleData.bind(this);
11 | // 생성 후 4초 후에 handleData를 호출합니다.
12 | setTimeout(this.handleData, 4000);
13 | }
14 | handleData() {
15 | const data = 'new data';
16 | // 상태 변경
17 | this.loading = false;
18 | this.formData = data + this.formData;
19 | this.forceUpdate();
20 | }
21 | render() {
22 | return (
23 |
24 | {/* 상태 데이터는 this.state로 접근 가능합니다. */}
25 | 로딩중: {String(this.loading)}
26 | 결과: {this.formData}
27 |
28 | );
29 | }
30 | }
31 |
32 | export default ForceUpdateExample;
33 |
--------------------------------------------------------------------------------
/src/sass/components/_materialbox.scss:
--------------------------------------------------------------------------------
1 | .materialboxed {
2 | &:hover {
3 | &:not(.active) {
4 | opacity: .8;
5 | }
6 | }
7 |
8 | display: block;
9 | cursor: zoom-in;
10 | position: relative;
11 | transition: opacity .4s;
12 | -webkit-backface-visibility: hidden;
13 |
14 | &.active {
15 | cursor: zoom-out;
16 | }
17 | }
18 |
19 | #materialbox-overlay {
20 | position:fixed;
21 | top: 0;
22 | right: 0;
23 | bottom: 0;
24 | left: 0;
25 | background-color: #292929;
26 | z-index: 1000;
27 | will-change: opacity;
28 | }
29 |
30 | .materialbox-caption {
31 | position: fixed;
32 | display: none;
33 | color: #fff;
34 | line-height: 50px;
35 | bottom: 0;
36 | left: 0;
37 | width: 100%;
38 | text-align: center;
39 | padding: 0% 15%;
40 | height: 50px;
41 | z-index: 1000;
42 | -webkit-font-smoothing: antialiased;
43 | }
--------------------------------------------------------------------------------
/src/02/02-2.js:
--------------------------------------------------------------------------------
1 | // ES5 문법
2 | var string1 = '안녕하세요';
3 | var string2 = '반갑습니다';
4 | var greeting = string1 + ' ' + string2;
5 | var product = { name: '도서', price: '4200원' };
6 | var message = '제품' + product.name + '의 가격은' + product.price + '입니다';
7 | var multiLine = '문자열1\n문자열2';
8 | var value1 = 1;
9 | var value2 = 2;
10 | var boolValue = false;
11 | var operator1 = '곱셈값은 ' + value1 * value2 + '입니다. ';
12 | var operator2 = '불리언값은 ' + (boolValue ? '참' : '거짓') + '입니다. ';
13 |
14 | //ES6 문법
15 | var string1 = '안녕하세요';
16 | var string2 = '반갑습니다';
17 | var greeting = `${string1} ${string2}`;
18 |
19 | var product = { name: '도서', price: '4200원' };
20 | var message = `제품 ${product.name}의 가격은 ${product.price}입니다`;
21 | var multiLine = `문자열1
22 | 문자열2`;
23 | var value1 = 1;
24 | var value2 = 2;
25 | var boolValue = false;
26 | var operator1 = `곱셈값은 ${value1 * value2}입니다.`;
27 | var operator2 = `불리언값은 ${boolValue ? '참' : '거짓'}입니다.`;
28 |
--------------------------------------------------------------------------------
/src/07/reducers/collectionReducer.js:
--------------------------------------------------------------------------------
1 | import { SET_COLLECTION, SET_AGE } from '../actions/collectionActions';
2 |
3 | const initState = {
4 | ids: [],
5 | entities: {},
6 | };
7 |
8 | export default (state = initState, action) => {
9 | const { type, payload } = action;
10 |
11 | switch (type) {
12 | case SET_COLLECTION: {
13 | const ids = payload.map(entity => entity['id']);
14 | const entities = payload.reduce((finalEntities, entity) => ({
15 | ...finalEntities,
16 | [entity['id']]: entity,
17 | }), {});
18 | return { ...state, ids, entities };
19 | }
20 | case SET_AGE: {
21 | const { id, age } = payload;
22 | return {
23 | ...state,
24 | entities: {
25 | ...state.entities,
26 | [id]: { ...state.entities[id], age },
27 | },
28 | };
29 | }
30 | default:
31 | return state;
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/src/03/ScrollSpy.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class ScrollSpy extends React.PureComponent {
4 | constructor(props) {
5 | super(props);
6 | this.setRef = this.setRef.bind(this);
7 | this.checkPosition = this.checkPosition.bind(this);
8 | }
9 | setRef(ref) {
10 | this.ref = ref;
11 | }
12 |
13 | checkPosition() {
14 | if (!this.ref) {
15 | return;
16 | }
17 |
18 | if (this.ref.getBoundingClientRect().top < window.innerHeight) {
19 | console.log('enter');
20 | } else {
21 | console.log('exit');
22 | }
23 | }
24 |
25 | componentDidMount() {
26 | window.addEventListener('scroll', this.checkPosition);
27 | this.checkPosition();
28 | }
29 |
30 | componentWillUnmount() {
31 | window.removeEventListener('scroll', this.checkPosition);
32 | }
33 |
34 | render() {
35 | return ;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/08/containers/main/TransactionPaginationContainer.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import TransactionPagination from '../../components/main/TransactionPagination';
3 | import { requestTransactionList } from '../../actions/transactionPackActions';
4 | import {
5 | paginationSelector,
6 | transactionListLoadingStateSelector,
7 | } from '../../selectors/transactionSelectors';
8 |
9 | const mapStateToProps = state => {
10 | const { pagination, loading, ids } = state.transactions;
11 | const { number, size } = pagination;
12 |
13 | return {
14 | searchParams: state.searchFilter.params,
15 | hasNext: ids.length === size,
16 | loading: transactionListLoadingStateSelector(state),
17 | pageNumber: paginationSelector(state).number || 1,
18 | };
19 | };
20 | const mapDispatchToProps = {
21 | requestTransactionList,
22 | };
23 |
24 | export default connect(mapStateToProps, mapDispatchToProps)(TransactionPagination);
25 |
--------------------------------------------------------------------------------
/src/stories/DoitUISpacingStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import Spacing from '../doit-ui/Spacing';
4 | import { css } from '../doit-ui/withStyles';
5 |
6 | function RedBox({ children }) {
7 | return {children}
;
8 | }
9 |
10 | storiesOf('Doit-UI/Spacing', module).addWithJSX('기본 설정', () => (
11 |
12 |
13 | top: 1
14 |
15 |
16 | bottom: 2
17 |
18 |
19 | left: 3
20 |
21 |
22 | right: 4
23 |
24 |
25 | horizontal: 5
26 |
27 |
28 | vertical: 6
29 |
30 |
31 | ));
32 |
--------------------------------------------------------------------------------
/src/07/SearchResultTable.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class SearchResultTable extends PureComponent {
5 | render() {
6 | const { items } = this.props;
7 | return (
8 |
9 |
10 | | 아이디 |
11 | 이름 |
12 | 나이 |
13 |
14 | {items.map(({ id, name, age }) => (
15 |
16 | | {id} |
17 | {name} |
18 | {age} |
19 |
20 | ))}
21 |
22 | );
23 | }
24 | }
25 |
26 | SearchResultTable.propTypes = {
27 | items: PropTypes.arrayOf(
28 | PropTypes.shape({
29 | id: PropTypes.number,
30 | name: PropTypes.name,
31 | age: PropTypes.number,
32 | }),
33 | ),
34 | };
35 | SearchResultTable.defaultProps = {
36 | items: [],
37 | };
38 | export default SearchResultTable;
39 |
--------------------------------------------------------------------------------
/src/02/02-3-2.js:
--------------------------------------------------------------------------------
1 | // ES5 예제
2 | var objectOne = { one: 1, two: 2, other: 0 };
3 | var objectTwo = { three: 3, four: 4, other: -1 };
4 |
5 | var combined = {
6 | one: objectOne.one,
7 | two: objectOne.two,
8 | three: objectTwo.three,
9 | four: objectTwo.four,
10 | };
11 | var combined = Object.assign({}, objectOne, objectTwo);
12 | // combined = { one: 1, two: 2, three: 3, four: 4, other: -1}
13 | var combined = Object.assign({}, objectOne, objectTwo);
14 | // combined = { one: 1, two: 2, three: 3, four: 4, other: 0}
15 | var others = Object.assign({}, combined);
16 | delete others.other;
17 |
18 | // ES6 예제
19 | var combined = {
20 | ...objectOne,
21 | ...objectTwo,
22 | };
23 | // combined = { one: 1, two: 2, three: 3, four: 4, other: -1}
24 | var combined = {
25 | ...objectTwo,
26 | ...objectOne,
27 | };
28 | // combined = { one: 1, two: 2, three: 3, four: 4, other: 0}
29 | var { other, ...others } = combined;
30 | // others = { one: 1, two: 2, three: 3, four: 4}
31 |
--------------------------------------------------------------------------------
/src/07/AdvReduxApp05.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Provider } from 'react-redux';
3 | import configureStore from './configureStore';
4 | import { setCollection } from './actions/collectionActions01';
5 | import { setAge } from './actions/collectionActions02';
6 |
7 | class AdvReduxApp extends PureComponent {
8 | store = configureStore({ loading: false });
9 |
10 | componentDidMount() {
11 | this.store.dispatch(
12 | setCollection([
13 | { id: 1, name: 'John', age: 20 },
14 | { id: 2, name: 'Park', age: 35 },
15 | ]),
16 | );
17 | this.store.dispatch(setAge(2, 55));
18 | const { collection } = this.store.getState();
19 | const { ids, entities } = collection;
20 | const originalPayload = ids.map(id => entities[id]);
21 | console.log(originalPayload);
22 | }
23 |
24 | render() {
25 | return 리덕스 예제;
26 | }
27 | }
28 | export default AdvReduxApp;
29 |
--------------------------------------------------------------------------------
/src/doit-ui/Card.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { withStyles, css, withStylesPropTypes } from './withStyles';
4 | import Spacing, { propTypes as spacingPropTypes } from './Spacing';
5 |
6 | class Card extends PureComponent {
7 | render() {
8 | const { children, styles, ...spacingProps } = this.props;
9 | return (
10 |
11 |
12 | {children}
13 |
14 |
15 | );
16 | }
17 | }
18 |
19 | Card.propTypes = {
20 | ...spacingPropTypes,
21 | ...withStylesPropTypes,
22 | children: PropTypes.node,
23 | };
24 |
25 | export default withStyles(({ depth, unit, color }) => ({
26 | wrapper: {
27 | ...depth.level1,
28 | borderRadius: unit,
29 | backgroundColor: color.white,
30 | display: 'flex',
31 | overflow: 'hidden',
32 | marginBottom: unit * 4,
33 | },
34 | }))(Card);
35 |
--------------------------------------------------------------------------------
/src/sass/components/forms/_file-input.scss:
--------------------------------------------------------------------------------
1 | /* File Input
2 | ========================================================================== */
3 |
4 | .file-field {
5 | position: relative;
6 |
7 | .file-path-wrapper {
8 | overflow: hidden;
9 | padding-left: 10px;
10 | }
11 |
12 | input.file-path { width: 100%; }
13 |
14 | .btn {
15 | float: left;
16 | height: $input-height;
17 | line-height: $input-height;
18 | }
19 |
20 | span {
21 | cursor: pointer;
22 | }
23 |
24 | input[type=file] {
25 |
26 | // Needed to override webkit button
27 | &::-webkit-file-upload-button {
28 | display: none;
29 | }
30 |
31 | position: absolute;
32 | top: 0;
33 | right: 0;
34 | left: 0;
35 | bottom: 0;
36 | width: 100%;
37 | margin: 0;
38 | padding: 0;
39 | font-size: 20px;
40 | cursor: pointer;
41 | opacity: 0;
42 | filter: alpha(opacity=0);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/03/NewCounter.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class NewCounter extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | this.state = {};
7 | this.increaseCount = this.increaseCount.bind(this);
8 | }
9 |
10 | static getDerivedStateFromProps(props, state) {
11 | const { count } = props;
12 | return {
13 | // 프로퍼티에서 전달된 count값을 보관한다
14 | count,
15 | newCount:
16 | count === state.count
17 | ? // count 프로퍼티가 변경되지 않으면 기존 state값으로 설정한다.
18 | state.newCount
19 | : // 초기 카운트값을 변경된 프로퍼티에서 값으로 설정한다.
20 | count,
21 | };
22 | }
23 |
24 | increaseCount() {
25 | this.setState(({ newCount }) => ({ newCount: newCount + 1 }));
26 | }
27 | render() {
28 | return (
29 |
30 | 현재 카운트: {this.state.newCount}
31 |
32 |
33 | );
34 | }
35 | }
36 |
37 | export default NewCounter;
38 |
--------------------------------------------------------------------------------
/src/03/ChildComponent.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class ChildComponent extends React.Component {
5 | render() {
6 | const {
7 | boolValue,
8 | numValue,
9 | arrayValue,
10 | objValue,
11 | nodeValue,
12 | funcValue,
13 | } = this.props;
14 |
15 | return (
16 |
17 | 불리언값: {boolValue}
18 | 숫자값: {numValue}
19 | 배열값: {arrayValue}
20 | 객체값: {String(objValue)}
21 | 노드값: {nodeValue}
22 | 함수값: {String(funcValue)}
23 |
24 | );
25 | }
26 | }
27 |
28 | ChildComponent.propTypes = {
29 | boolValue: PropTypes.bool,
30 | numValue: PropTypes.number,
31 | arrayValue: PropTypes.arrayOf(PropTypes.number),
32 | objValue: PropTypes.object,
33 | nodeValue: PropTypes.node,
34 | funcValue: PropTypes.func,
35 | }
36 |
37 | export default ChildComponent;
38 |
--------------------------------------------------------------------------------
/src/02/02-5.js:
--------------------------------------------------------------------------------
1 | function add(first, second) {
2 | return first + second;
3 | }
4 |
5 | var add = function(first, second) {
6 | return first + second;
7 | };
8 |
9 | var add = function add(first, second) {
10 | return first + second;
11 | };
12 |
13 | // this scope를 전달한 예
14 | var self = this;
15 | var addThis = function(first, second) {
16 | return self.value + first + second;
17 | };
18 |
19 | var addThis = (first, second) => first + second;
20 |
21 | function addNumber(num) {
22 | return function(value) {
23 | return num + value;
24 | };
25 | }
26 | // 화살표 함수로 변환한 예
27 | var addNumber = num => value => num + value;
28 |
29 | var addTwo = addNumber(2);
30 | var result = addTwo(4); // 6
31 |
32 | // bind함수를 통해 this scope를 전달한 예
33 | class MyClass {
34 | value = 10;
35 | constructor() {
36 | var addThis2 = function(first, second) {
37 | return this.value + first + second;
38 | }.bind(this);
39 | var addThis3 = (first, second) => this.value + first + second;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/02/throttle.js:
--------------------------------------------------------------------------------
1 | export function throttle(func, delay) {
2 | let lastFunc;
3 | let lastRan;
4 | return function(...args) {
5 | const context = this;
6 | if (!lastRan) {
7 | func.call(context, ...args);
8 | lastRan = Date.now();
9 | } else {
10 | if (lastFunc) clearTimeout(lastFunc);
11 | lastFunc = setTimeout(function() {
12 | if ((Date.now() - lastRan) >= delay) {
13 | func.call(context, ...args);
14 | lastRan = Date.now();
15 | }
16 | }, delay - (Date.now() - lastRan));
17 | }
18 | }
19 | }
20 |
21 | var checkPosition = () => {
22 | const offset = 500;
23 | const currentScrollPosition = window.pageYOffset;
24 | const pageBottomPosition = document.body.offsetHeight - window.innerHeight - offset;
25 | if (currentScrollPosition >= pageBottomPosition) {
26 | // fetch('/page/next');
27 | console.log('다음 페이지 로딩');
28 | }
29 | };
30 | var infiniteScroll = throttle(checkPosition, 300);
31 | window.addEventListener('scroll', infiniteScroll);
32 |
--------------------------------------------------------------------------------
/src/06/CreateMemberModalContent.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Consumer } from './ModalContext';
3 | import Button from '../04/Button';
4 | import Text from '../04/Text';
5 | import Input from '../03/Input';
6 |
7 | class CreateMemberModalContent extends PureComponent {
8 | render() {
9 | return (
10 |
11 | {({ closeModal }) => (
12 |
13 |
25 |
26 |
27 |
28 | )}
29 |
30 | );
31 | }
32 | }
33 |
34 | export default CreateMemberModalContent;
35 |
--------------------------------------------------------------------------------
/src/02/02-3.js:
--------------------------------------------------------------------------------
1 | // ES5 문법
2 | var array1 = ['one', 'two'];
3 | var array2 = ['three', 'four'];
4 | var combined = [array1[0], array1[1], array2[0], array2[1]];
5 | var combined = array1.concat(array2);
6 | var combined = [].concat(array1, array2);
7 | var first = array1[0];
8 | var second = array1[1];
9 | var three = array1[2] || 'empty';
10 |
11 | function func() {
12 | var args = Array.prototype.slice.call(this, arguments);
13 | var first = args[0];
14 | var others = args.slice(1);
15 | }
16 |
17 | // ES6 문법
18 | var array1 = ['one', 'two'];
19 | var array2 = ['three', 'four'];
20 | var combined = [...array1, ...array2];
21 | // combined = ['one', 'two', 'three', 'four'];
22 | var [first, second, three = 'empty', ...others] = array1;
23 | // first = 'one', second = 'two', three = 'empty', others = []
24 |
25 | function func(...args) {
26 | var [first, ...others] = args;
27 | }
28 |
29 | function func(first, ...others) {
30 | var firstInES6 = first;
31 | var othersInES6 = others;
32 | }
33 |
34 | // 올바르지 못한 예
35 | // var wrongArr = ...array1;
36 |
--------------------------------------------------------------------------------
/src/06/LoadingProviderWithKey.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export const DEFAULT_KEY = 'defaultLoadingKey';
5 | export const contextPropTypes = {
6 | loading: PropTypes.bool,
7 | setLoading: PropTypes.func,
8 | };
9 |
10 | export default (contextKey = DEFAULT_KEY) => {
11 | class LoadingProvider extends React.Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | this.state = { loading: false };
16 | this.setLoading = this.setLoading.bind(this);
17 | }
18 |
19 | getChildContext() {
20 | return {
21 | [contextKey]: {
22 | loading: this.state.loading,
23 | setLoading: this.setLoading,
24 | },
25 | };
26 | }
27 |
28 | setLoading(loading) {
29 | this.setState({ loading });
30 | }
31 |
32 | render() {
33 | return this.props.children;
34 | }
35 | }
36 |
37 | LoadingProvider.childContextTypes = {
38 | [contextKey]: contextPropTypes,
39 | };
40 |
41 | return LoadingProvider;
42 | };
43 |
--------------------------------------------------------------------------------
/src/06/Modal.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import withStyles, { css } from '../04/withStyles';
4 |
5 | class Modal extends PureComponent {
6 | render() {
7 | const { styles, children } = this.props;
8 | return (
9 |
10 |
11 |
12 | {children}
13 |
14 |
15 |
16 | );
17 | }
18 | }
19 |
20 | Modal.propTypes = {
21 | children: PropTypes.node,
22 | };
23 |
24 | export default withStyles(({ color, unit }) => ({
25 | overlay: {
26 | position: 'fixed',
27 | zIndex: 9999,
28 | top: 0,
29 | left: 0,
30 | width: '100%',
31 | height: '100%',
32 | backgroundColor: 'rgba(0, 0, 0, .5)',
33 | },
34 | wrapper: {
35 | verticalAlign: 'middle',
36 | },
37 | container: {
38 | margin: '40px auto 0px',
39 | padding: unit * 4,
40 | backgroundColor: color.white,
41 | width: 400,
42 | },
43 | }))(Modal);
44 |
--------------------------------------------------------------------------------
/src/06/ButtonWithNewConsumer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from '../04/Button';
4 | import { Consumer } from './LoadingProviderWithNewContext';
5 |
6 | function ButtonWithNewConsumer({ label }) {
7 | return (
8 |
9 |
10 | {value => (
11 |
14 | )}
15 |
16 |
17 | {({ loading2, setLoading }) => (
18 |
21 | )}
22 |
23 |
24 | {({ loading, loading2 }) => }
25 |
26 |
27 | );
28 | }
29 |
30 | ButtonWithNewConsumer.propTypes = {
31 | label: PropTypes.string,
32 | };
33 |
34 | export default ButtonWithNewConsumer;
35 |
--------------------------------------------------------------------------------
/src/05/05-1-currying.js:
--------------------------------------------------------------------------------
1 | function multiply(a, b) {
2 | return a * b;
3 | }
4 |
5 | function multiplyTwo(a) {
6 | return multiply(a, 2);
7 | }
8 |
9 | const multiplyX = x => a => multiply(a, x);
10 |
11 | // a.k.a
12 | function multiplyX(x) {
13 | return function(a) {
14 | return multiply(a, x);
15 | }
16 | }
17 |
18 | const multiplyThree = multiplyX(3);
19 | const multiplyFour = multiplyX(4);
20 |
21 | // x
22 | // => x * 2
23 | // => (x * 2) * 3
24 | // => ((x * 2) * 3) + 4
25 |
26 | const equation = (a, b, c) => x => ((x * a) * b) + c;
27 | const formula = equation(2, 3, 4);
28 | const x = 2;
29 | const result = formula(x);
30 |
31 | const multiply = (a, b) => a * b;
32 | const add = (a, b) => a + b;
33 |
34 | const multiplyX = x => a => multiply(a, 2);
35 | const addX = x => a => add(x, a);
36 | const addFour = addX(4);
37 | const multiplyTwo = multiplyX(2);
38 | const multiplyThree = multiplyX(3);
39 |
40 |
41 | // => ((x * 2) * 3) + 4
42 | const formula = x => addFour(multiplyThree(multiplyTwo(x)));
43 |
44 | // => ((x + 4) * 3) * 2
45 | const formulaB = x => multiplyTwo(multiplyThree(addFour(x)));
46 |
--------------------------------------------------------------------------------
/src/06/HomePageWithTwoProvider.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import LoadingProviderWithKey from './LoadingProviderWithKey';
3 | import {
4 | ButtonWithDefaultLoadingContext,
5 | ButtonWithLoading2Context,
6 | } from './ButtonWithLoadingContextAndKey';
7 | import ButtonWithConsumer from './ButtonWithConsumer';
8 |
9 | const LoadingProvider1 = LoadingProviderWithKey();
10 | const LoadingProvider2 = LoadingProviderWithKey('loading2');
11 | function TableComponent() {
12 | return (
13 |
14 | 컨텍스트1
15 | 컨텍스트2
16 | 다중 소비자 예제
17 |
18 | );
19 | }
20 |
21 | class HomePageComponentWithTwoProvider extends PureComponent {
22 | render() {
23 | return (
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 | }
32 |
33 | export default HomePageComponentWithTwoProvider;
34 |
--------------------------------------------------------------------------------
/src/03/LifecycleExample.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class LifecycleExample extends React.Component {
4 | static getDerivedStateFromProps() {
5 | console.log('getDerivedStateFromProps 호출');
6 | return {};
7 | }
8 | constructor(props) {
9 | super(props);
10 | // getDerivedStateFromProps를 사용하기 때문에
11 | // 경고 메세지를 건너뛰기위해 초기 상태를 설정합니다.
12 | this.state = {};
13 | console.log('constructor 호출');
14 | }
15 | componentDidMount() {
16 | console.log('componentDidMount 호출');
17 | // this.setState({ updated: true});
18 | this.forceUpdate();
19 | }
20 | componentDidUpdate() {
21 | console.log('componentDidUpdate 호출');
22 | }
23 | componentWillUnmount() {
24 | console.log('componentWillUnmount 호출');
25 | }
26 | getSnapshotBeforeUpdate() {
27 | console.log('getSnapshotBeforeUpdate 호출');
28 | return {};
29 | }
30 | shouldComponentUpdate() {
31 | console.log('shouldComponentUpdate 호출');
32 | return false;
33 | }
34 | render() {
35 | console.log('render 호출');
36 | return null;
37 | }
38 | }
39 |
40 | export default LifecycleExample;
41 |
--------------------------------------------------------------------------------
/src/doit-ui/Modal/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Card from '../Card';
4 | import { css, withStyles } from '../withStyles';
5 |
6 | class Modal extends PureComponent {
7 | render() {
8 | const { styles, children } = this.props;
9 | return (
10 |
11 |
12 |
13 |
14 | {children}
15 |
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | Modal.propTypes = {
24 | children: PropTypes.node,
25 | };
26 |
27 | export default withStyles(() => ({
28 | overlay: {
29 | position: 'fixed',
30 | zIndex: 9999,
31 | top: 0,
32 | left: 0,
33 | width: '100%',
34 | height: '100%',
35 | backgroundColor: 'rgba(0, 0, 0, .5)',
36 | },
37 | wrapper: {
38 | verticalAlign: 'middle',
39 | },
40 | container: {
41 | margin: '40px auto 0px',
42 | width: 700,
43 | },
44 | }))(Modal);
45 |
--------------------------------------------------------------------------------
/src/05/withState.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import withState from 'recompose/withState';
3 | import withHandlers from 'recompose/withHandlers';
4 | import Button from '../04/Button';
5 |
6 | export const withCountState = withState('count', 'setCount', 0);
7 | export const withCountHandlers = withHandlers({
8 | increaseCount: ({ setCount }) => () => setCount(value => value + 1),
9 | decreaseCount: ({ setCount }) => () => setCount(value => value - 1),
10 | resetCount: ({ setCount }) => () => setCount(0),
11 | });
12 |
13 | function Counter({ count, setCount }) {
14 | const increaseCount = () => setCount(value => value + 1);
15 |
16 | return (
17 |
18 | 현재 카운트: {count}
19 |
20 |
21 | );
22 | }
23 |
24 | function Counter2({ count, increaseCount }) {
25 | return (
26 |
27 | 현재 카운트: {count}
28 |
29 |
30 | );
31 | }
32 |
33 | export const CounterWithCountState = withCountState(Counter);
34 | export const CounterWithCountHandler = withCountState(withCountHandlers(Counter2));
35 |
--------------------------------------------------------------------------------
/src/04/Theme.js:
--------------------------------------------------------------------------------
1 | export const LARGE_AND_ABOVE = 'largeAndAbove';
2 | const BREAKPOINT_NAMES = {
3 | LARGE: 'large',
4 | MEDIUM: 'medium',
5 | SMALL: 'small',
6 | };
7 |
8 | const breakpoints = {
9 | [BREAKPOINT_NAMES.LARGE]: 1128,
10 | [BREAKPOINT_NAMES.MEDIUM]: 744,
11 | [BREAKPOINT_NAMES.SMALL]: 327,
12 | };
13 |
14 | const responsive = {
15 | [LARGE_AND_ABOVE]: `@media (min-width: ${breakpoints[BREAKPOINT_NAMES.LARGE]}px)`,
16 | [BREAKPOINT_NAMES.SMALL]: `@media (max-width: ${breakpoints[BREAKPOINT_NAMES.MEDIUM] - 1}px)`,
17 | print: '@media print',
18 | };
19 |
20 | export default {
21 | // 색상
22 | color: {
23 | primary: '#03a9f4', // 주 색상
24 | secondary: '#795548', // 부 색상
25 | white: '#FFFFFF',
26 | gray: '#CCCCCC',
27 | default: '#999999', // 기본 문자 색상
28 | error: '#FF0000', // 오류 색상
29 | },
30 | // 폰트 사이즈
31 | size: {
32 | xg: 24,
33 | lg: 18,
34 | md: 14,
35 | sm: 12,
36 | xs: 10,
37 | },
38 | lineHeight: {
39 | xg: '60px',
40 | lg: '54px',
41 | md: '36px',
42 | sm: '24px',
43 | xs: '18px',
44 | },
45 | // 길이 단위
46 | unit: 4,
47 | // 반응형 미디어 속성
48 | responsive,
49 | };
50 |
--------------------------------------------------------------------------------
/src/05/withError.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import withStyles, { css } from '../04/withStyles';
3 |
4 | // TODO: withError HoC만들기
5 | // hasError, errorMessage
6 | // hasError true => errorMessage 페인트
7 | // 만약 errorMessage가 없으면 defaultMessage를 뿌려주기 (defaultProps)
8 | // 컴포넌트 원래대로 그리고 대신 그 밑에 에러메세지를 찍어주기
9 |
10 | export default function(defaultMessage) {
11 | return WrappedComponent => {
12 | const { displayName, name: componentName } = WrappedComponent;
13 | const wrappedComponentName = displayName || componentName;
14 |
15 | function ComponentWithError({ hasError, errorMessage, styles, ...props }) {
16 | return (
17 |
18 |
19 | {hasError && {errorMessage}
}
20 |
21 | );
22 | }
23 | ComponentWithError.defaultProps = {
24 | errorMessage: defaultMessage,
25 | };
26 |
27 | ComponentWithError.displayName = `withError(${wrappedComponentName})`;
28 | return withStyles(({ color }) => ({
29 | error: {
30 | color: color.error,
31 | },
32 | }))(ComponentWithError);
33 | };
34 | }
35 |
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "description": "Cloud Functions for Firebase",
4 | "scripts": {
5 | "serve": "firebase serve --only functions",
6 | "shell": "firebase functions:shell",
7 | "start": "npm run shell",
8 | "deploy": "firebase deploy --only functions",
9 | "logs": "firebase functions:log"
10 | },
11 | "dependencies": {
12 | "aphrodite": "^2.3.1",
13 | "axios": "^0.18.0",
14 | "body-parser": "^1.18.3",
15 | "express": "^4.16.4",
16 | "firebase-admin": "~7.0.0",
17 | "firebase-functions": "^2.2.0",
18 | "moment": "^2.24.0",
19 | "next": "^8.1.0",
20 | "react": "^16.8.6",
21 | "react-dom": "^16.8.6",
22 | "react-redux": "^6.0.1",
23 | "react-router-dom": "^5.0.0",
24 | "react-with-styles": "^3.2.1",
25 | "react-with-styles-interface-aphrodite": "^5.0.1",
26 | "recompose": "^0.30.0",
27 | "redux": "^4.0.1",
28 | "redux-devtools-extension": "^2.13.8",
29 | "redux-pack": "^0.1.5",
30 | "redux-thunk": "^2.3.0",
31 | "reselect": "^4.0.0",
32 | "selector-action": "^1.1.1"
33 | },
34 | "engines": {
35 | "node": "8"
36 | },
37 | "private": true
38 | }
39 |
--------------------------------------------------------------------------------
/src/07/ActionComponent02.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from '../04/Button';
4 |
5 | class AdvActionComponent extends PureComponent {
6 | render() {
7 | const { setLoading, resetLoading, setUser, setCollection, setAge } = this.props;
8 | const collection = [
9 | { id: 21, name: 'John', age: 20 },
10 | { id: 2, name: 'Justin', age: 40 },
11 | { id: 3, name: 'Mary', age: 21 },
12 | ];
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | AdvActionComponent.propTypes = {
26 | setLoading: PropTypes.func,
27 | resetLoading: PropTypes.func,
28 | setUser: PropTypes.func,
29 | setCollection: PropTypes.func,
30 | setAge: PropTypes.func,
31 | };
32 |
33 | export default AdvActionComponent;
34 |
--------------------------------------------------------------------------------
/src/stories/DoitUISelectStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { action } from '@storybook/addon-actions';
4 |
5 | import Select, { Option } from '../doit-ui/Select';
6 |
7 | storiesOf('Doit-UI/Select', module)
8 | .addWithJSX('기본 설정', () => (
9 |
12 | ))
13 | .addWithJSX('label 예제', () => (
14 |
17 | ))
18 | .addWithJSX('onChange 예제', () => (
19 |
22 | ))
23 | .addWithJSX('value 예제', () => (
24 |
27 | ))
28 | .addWithJSX('errorMessage 예제', () => (
29 |
32 | ))
33 | .addWithJSX('autoFocus 예제', () => (
34 |
37 | ));
38 |
--------------------------------------------------------------------------------
/src/stories/ChecBoxStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { action } from '@storybook/addon-actions';
4 |
5 | import CheckBox from '../04/CheckBox';
6 | import Text from '../04/Text';
7 |
8 | storiesOf('CheckBox', module)
9 | .addWithJSX('기본 설정', () => )
10 | .addWithJSX('children 예제', () => (
11 |
12 | 동의합니다
13 |
14 | ))
15 | .addWithJSX('label 예제', () => (
16 |
17 | 동의합니다
18 |
19 | ))
20 | .addWithJSX('onChange 예제', () => (
21 |
22 | 동의합니다
23 |
24 | ))
25 | .addWithJSX('checked 예제', () => (
26 |
27 | 동의합니다
28 |
29 | ))
30 | .addWithJSX('errorMessage 예제', () => (
31 |
32 | 동의합니다
33 |
34 | ))
35 | .addWithJSX('autoFocus 예제', () => (
36 |
37 | 동의합니다
38 |
39 | ));
40 |
--------------------------------------------------------------------------------
/src/__tests__/05/withError.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 |
4 | import Input from '../../04/InputWithStyle';
5 | import withError from '../../05/withError';
6 |
7 | const errorMessage = '문제 발생';
8 | const InputWithError = withError(errorMessage)(Input);
9 |
10 | describe('withError', () => {
11 | it('renders without crashing', () => {
12 | expect(() => {
13 | shallow().dive();
14 | }).not.toThrow();
15 | });
16 |
17 | it('displays displayName withError', () => {
18 | expect(InputWithError.displayName).toEqual(`withStyles(withError(${Input.name}))`);
19 | });
20 |
21 | it('renders default error message with hasError', () => {
22 | const wrapper = shallow().dive();
23 | expect(wrapper.text()).toContain(errorMessage);
24 | expect(wrapper.find(Input)).toHaveLength(1);
25 | });
26 |
27 | it('renders custom errorMessage with hasError', () => {
28 | const customErrorMessage = '필수입력 항목입니다';
29 | const wrapper = shallow(
30 | ,
31 | ).dive();
32 | expect(wrapper.text()).toContain(customErrorMessage);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/sass/components/_toast.scss:
--------------------------------------------------------------------------------
1 | #toast-container {
2 | display:block;
3 | position: fixed;
4 | z-index: 10000;
5 |
6 | @media #{$small-and-down} {
7 | min-width: 100%;
8 | bottom: 0%;
9 | }
10 | @media #{$medium-only} {
11 | left: 5%;
12 | bottom: 7%;
13 | max-width: 90%;
14 | }
15 | @media #{$large-and-up} {
16 | top: 10%;
17 | right: 7%;
18 | max-width: 86%;
19 | }
20 | }
21 |
22 | .toast {
23 | @extend .z-depth-1;
24 | border-radius: 2px;
25 | top: 35px;
26 | width: auto;
27 | margin-top: 10px;
28 | position: relative;
29 | max-width:100%;
30 | height: auto;
31 | min-height: $toast-height;
32 | line-height: 1.5em;
33 | background-color: $toast-color;
34 | padding: 10px 25px;
35 | font-size: 1.1rem;
36 | font-weight: 300;
37 | color: $toast-text-color;
38 | display: flex;
39 | align-items: center;
40 | justify-content: space-between;
41 | cursor: default;
42 |
43 | .toast-action {
44 | color: $toast-action-color;
45 | font-weight: 500;
46 | margin-right: -25px;
47 | margin-left: 3rem;
48 | }
49 |
50 | &.rounded{
51 | border-radius: 24px;
52 | }
53 |
54 | @media #{$small-and-down} {
55 | width: 100%;
56 | border-radius: 0;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/__tests__/05/withLoading.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 |
4 | import Button from '../../04/Button';
5 | import withLoading from '../../05/withLoading';
6 |
7 | const ButtonWithLoading = withLoading()(Button);
8 |
9 | describe('withLoading', () => {
10 | it('renders without crashing', () => {
11 | expect(() => {
12 | shallow(테스트);
13 | }).not.toThrow();
14 | });
15 |
16 | it('displays displayName withLoading', () => {
17 | expect(ButtonWithLoading.displayName).toEqual(`withLoading(${Button.displayName})`);
18 | });
19 |
20 | it('renders default loading message with isLoading', () => {
21 | const wrapper = shallow(테스트);
22 | expect(wrapper.text()).toEqual('로딩중');
23 | expect(wrapper.find(Button)).toHaveLength(0);
24 | });
25 |
26 | it('renders loadingMessage with isLoading', () => {
27 | const customLoadingMessage = '기다려주세요...';
28 | const ButtonWithLoadingMessage = withLoading(customLoadingMessage)(Button);
29 | const wrapper = shallow(테스트);
30 | expect(wrapper.text()).toEqual(customLoadingMessage);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/sass/materialize.scss:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | // Color
4 | @import "components/color-variables";
5 | @import "components/color-classes";
6 |
7 | // Variables;
8 | @import "components/variables";
9 |
10 | // Reset
11 | @import "components/normalize";
12 |
13 | // components
14 | @import "components/global";
15 | @import "components/badges";
16 | @import "components/icons-material-design";
17 | @import "components/grid";
18 | @import "components/navbar";
19 | @import "components/typography";
20 | @import "components/transitions";
21 | @import "components/cards";
22 | @import "components/toast";
23 | @import "components/tabs";
24 | @import "components/tooltip";
25 | @import "components/buttons";
26 | @import "components/dropdown";
27 | @import "components/waves";
28 | @import "components/modal";
29 | @import "components/collapsible";
30 | @import "components/chips";
31 | @import "components/materialbox";
32 | @import "components/forms/forms";
33 | @import "components/table_of_contents";
34 | @import "components/sidenav";
35 | @import "components/preloader";
36 | @import "components/slider";
37 | @import "components/carousel";
38 | @import "components/tapTarget";
39 | @import "components/pulse";
40 | @import "components/datepicker";
41 | @import "components/timepicker";
42 |
--------------------------------------------------------------------------------
/src/07/AdvReduxApp06.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Provider } from 'react-redux';
3 | import configureStore from './configureStore';
4 | import { setCollection } from './actions/collectionActions01';
5 | import { setAge } from './actions/collectionActions02';
6 |
7 | import ContainerComponent from './containers/ContainerComponent';
8 | import PresentationComponent from './PresentationComponent';
9 |
10 | class AdvReduxApp extends PureComponent {
11 | store = configureStore({ loading: false });
12 |
13 | componentDidMount() {
14 | this.store.dispatch(
15 | setCollection([
16 | { id: 1, name: 'John', age: 20 },
17 | { id: 2, name: 'Park', age: 35 },
18 | ]),
19 | );
20 | this.store.dispatch(setAge(2, 55));
21 | const { collection } = this.store.getState();
22 | const { ids, entities } = collection;
23 | const originalPayload = ids.map(id => entities[id]);
24 | console.log(originalPayload);
25 | }
26 |
27 | render() {
28 | return (
29 |
30 | 화면 컴포넌트:
31 |
32 | 데이터 컴포넌트:
33 |
34 | );
35 | }
36 | }
37 | export default AdvReduxApp;
38 |
--------------------------------------------------------------------------------
/src/__tests__/04/CheckBox.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 |
4 | import CheckBox from '../../04/CheckBox';
5 |
6 | describe('', () => {
7 | it('renders without crashing', () => {
8 | expect(() => {
9 | shallow(테스트);
10 | }).not.toThrow();
11 | });
12 |
13 | it('displays errorMessage', () => {
14 | const errorMessage = '오류 메시지';
15 | const errorHtml = shallow(테스트)
16 | .dive()
17 | .find('span')
18 | .html();
19 | expect(errorHtml).toContain(errorMessage);
20 | });
21 |
22 | it('calls back onChange on input is clicked', () => {
23 | const changeStub = jest.fn();
24 | expect(changeStub).toHaveBeenCalledTimes(0);
25 | const input = shallow(테스트)
26 | .dive()
27 | .find('input');
28 | expect(input).toHaveLength(1);
29 | input.simulate('click', { target: { checked: true } });
30 | expect(changeStub).toHaveBeenCalledTimes(1);
31 | expect(changeStub).toHaveBeenCalledWith('test_name', true);
32 | input.simulate('click', { target: { checked: false } });
33 | expect(changeStub).toHaveBeenCalledWith('test_name', false);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/sass/components/_badges.scss:
--------------------------------------------------------------------------------
1 | // Badges
2 | span.badge {
3 | min-width: 3rem;
4 | padding: 0 6px;
5 | margin-left: 14px;
6 | text-align: center;
7 | font-size: 1rem;
8 | line-height: $badge-height;
9 | height: $badge-height;
10 | color: color('grey', 'darken-1');
11 | float: right;
12 | box-sizing: border-box;
13 |
14 | &.new {
15 | font-weight: 300;
16 | font-size: 0.8rem;
17 | color: #fff;
18 | background-color: $badge-bg-color;
19 | border-radius: 2px;
20 | }
21 | &.new:after {
22 | content: " new";
23 | }
24 |
25 | &[data-badge-caption]::after {
26 | content: " " attr(data-badge-caption);
27 | }
28 | }
29 |
30 | // Special cases
31 | nav ul a span.badge {
32 | display: inline-block;
33 | float: none;
34 | margin-left: 4px;
35 | line-height: $badge-height;
36 | height: $badge-height;
37 | -webkit-font-smoothing: auto;
38 | }
39 |
40 | // Line height centering
41 | .collection-item span.badge {
42 | margin-top: calc(#{$collection-line-height / 2} - #{$badge-height / 2});
43 | }
44 | .collapsible span.badge {
45 | margin-left: auto;
46 | }
47 | .sidenav span.badge {
48 | margin-top: calc(#{$sidenav-line-height / 2} - #{$badge-height / 2});
49 | }
50 |
51 | table span.badge {
52 | display: inline-block;
53 | float: none;
54 | margin-left: auto;
55 | }
56 |
--------------------------------------------------------------------------------
/src/08/containers/main/TransactionListContainer.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import TransactionList from '../../components/main/TransactionList';
3 | // import { setTransactionList } from '../../actions/transactionActions';
4 | // import { requestTransactionList } from '../../actions/transactionActions';
5 | import { requestTransactionList } from '../../actions/transactionPackActions';
6 | import {
7 | transactionListSelector,
8 | transactionListLoadingStateSelector,
9 | } from '../../selectors/transactionSelectors';
10 |
11 | // const mapStateToProps = state => {
12 | // // const { ids, entities, loadingState, pages, pagination } = state.transactions;
13 | // // const transactions = ids.map(id => entities[id]);
14 | // const { pagination } = state.transactions;
15 | // const transactions = transactionListSelector(state);
16 | // const loading = transactionListLoadingStateSelector(state);
17 | // const { number = 1 } = pagination;
18 | // return { transactions, loading: loading && number === 1 };
19 | // };
20 |
21 | const mapStateToProps = state => ({
22 | transactions: transactionListSelector(state),
23 | loading: transactionListLoadingStateSelector(state),
24 | });
25 |
26 | const mapDispatchToProps = {
27 | requestTransactionList,
28 | };
29 |
30 | export default connect(mapStateToProps, mapDispatchToProps)(TransactionList);
31 |
--------------------------------------------------------------------------------
/src/03/StateExample.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class StateExample extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | // 상태 정의
7 | this.state = {
8 | loading: true,
9 | formData: 'no data',
10 | };
11 | // 이후 콜백 함수를 다룰때 bind를 선언하는 부분에 대해 다룹니다
12 | this.handleData = this.handleData.bind(this);
13 | // 생성 후 4초 후에 handleData를 호출합니다.
14 | setTimeout(this.handleData, 4000);
15 | }
16 | handleData() {
17 | const data = 'new data';
18 | const { formData } = this.state;
19 | // 상태 변경
20 | this.setState({
21 | loading: false,
22 | formData: data + formData,
23 | });
24 | // this.state.loading 은 현재 true 입니다.
25 | // 이후 호출될 출력함수에서의 this.state.loading은 false입니다.
26 | }
27 | // 다음과 같이 setState함수를 사용할 수 있습니다.
28 | // handleData(data) {
29 | // this.setState(function(prevState) {
30 | // const newState = {
31 | // loading : false,
32 | // formData: data + prevState.formData,
33 | // };
34 | // return newState;
35 | // });
36 | // }
37 | render() {
38 | return (
39 |
40 | {/* 상태 데이터는 this.state로 접근 가능합니다. */}
41 | 로딩중: {String(this.state.loading)}
42 | 결과: {this.state.formData}
43 |
44 | );
45 | }
46 | }
47 |
48 | export default StateExample;
49 |
--------------------------------------------------------------------------------
/src/doit-ui/Spacing.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { css } from './withStyles';
4 | import { unit } from './Theme';
5 |
6 | export const propTypes = {
7 | top: PropTypes.number,
8 | left: PropTypes.number,
9 | right: PropTypes.number,
10 | bottom: PropTypes.number,
11 | vertical: PropTypes.number,
12 | horizontal: PropTypes.number
13 | };
14 |
15 | class Spacing extends PureComponent {
16 | render() {
17 | const {
18 | children,
19 | top,
20 | left,
21 | right,
22 | bottom,
23 | vertical,
24 | horizontal
25 | } = this.props;
26 | const computedTop = top ? top : vertical;
27 | const computedBottom = bottom ? bottom : vertical;
28 | const computedLeft = left ? left : horizontal;
29 | const computedRight = right ? right : horizontal;
30 |
31 | const computedStyles = {
32 | flex: 1,
33 | ...(computedTop && { marginTop: computedTop * unit }),
34 | ...(computedBottom && { marginBottom: computedBottom * unit }),
35 | ...(computedLeft && { marginLeft: computedLeft * unit }),
36 | ...(computedRight && { marginRight: computedRight * unit })
37 | };
38 |
39 | return {children}
;
40 | }
41 | }
42 |
43 | Spacing.propTypes = propTypes;
44 |
45 | export default Spacing;
46 |
--------------------------------------------------------------------------------
/src/07/ReduxApp03.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Provider } from 'react-redux';
3 | import { createStore } from 'redux';
4 |
5 | const reducer = (state, action) => {
6 | const { type, payload } = action;
7 | switch(type) {
8 | case 'SET_LOADING': {
9 | return {
10 | ...state,
11 | loading: payload,
12 | };
13 | }
14 | case 'RESET_LOADING': {
15 | return { ...state, loading: false };
16 | }
17 | case 'SET_USER': {
18 | return {
19 | ...state,
20 | user: payload,
21 | };
22 | }
23 | default:
24 | return state;
25 | }
26 | };
27 |
28 | class ReduxApp extends PureComponent {
29 | store = createStore(
30 | reducer,
31 | { loading: false, name: '두잇 리액트' },
32 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
33 | );
34 |
35 | componentDidMount() {
36 | this.store.dispatch({
37 | type: 'SET_LOADING',
38 | payload: true,
39 | });
40 | this.store.dispatch({
41 | type: 'RESET_LOADING'
42 | });
43 | this.store.dispatch({
44 | type: 'SET_USER',
45 | payload: { name: 'Park', age: 20 },
46 | });
47 | }
48 |
49 | render() {
50 | return (
51 |
52 | 리덕스 예제
53 |
54 | );
55 | }
56 | }
57 | export default ReduxApp;
58 |
--------------------------------------------------------------------------------
/src/doit-ui/Toast.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { withStyles, css, withStylesPropTypes } from './withStyles';
4 | import Spacing from './Spacing';
5 | import Text from './Text';
6 |
7 | class Toast extends PureComponent {
8 | render() {
9 | const { message, styles, warning } = this.props;
10 |
11 | return (
12 |
13 |
14 |
15 |
16 | {message}
17 |
18 |
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | Toast.propTypes = {
26 | ...withStylesPropTypes,
27 | warning: PropTypes.bool,
28 | message: PropTypes.string,
29 | };
30 |
31 | export default withStyles(({ depth, unit, color }) => ({
32 | overlay: {
33 | position: 'fixed',
34 | bottom: 0,
35 | right: 0,
36 | margin: unit * 4,
37 | },
38 | wrapper: {
39 | ...depth.level1,
40 | borderRadius: unit,
41 | backgroundColor: color.secondary,
42 | paddingTop: unit * 2,
43 | paddingBottom: unit * 2,
44 | paddingRight: unit * 4,
45 | paddingLeft: unit * 4,
46 | marginBottom: unit * 4,
47 | },
48 | warning: {
49 | backgroundColor: color.error,
50 | },
51 | }))(Toast);
52 |
--------------------------------------------------------------------------------
/src/06/createModalProvider.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import Modal from './Modal';
3 | import { Provider } from './ModalContext';
4 |
5 | export default function createModalProvider(ContentMap = {}) {
6 | return class ModalProvider extends PureComponent {
7 | constructor(props) {
8 | super(props);
9 |
10 | this.state = { showModal: false };
11 | this.handleClose = this.handleClose.bind(this);
12 | this.handleOpen = this.handleOpen.bind(this);
13 | }
14 |
15 | handleOpen(contentId, modalProps) {
16 | this.contentId = contentId;
17 | this.modalProps = modalProps;
18 | this.setState({ showModal: true });
19 | }
20 |
21 | handleClose() {
22 | this.setState({ showModal: false });
23 | }
24 |
25 | render() {
26 | const { children } = this.props;
27 | const { showModal } = this.state;
28 | const ModalContent = ContentMap[this.contentId];
29 |
30 | return (
31 |
37 | {children}
38 | {showModal && ModalContent && (
39 |
40 |
41 |
42 | )}
43 |
44 | );
45 | }
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/src/07/AdvReduxApp07.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Provider } from 'react-redux';
3 | import configureStore from './configureStore';
4 | import { setCollection } from './actions/collectionActions01';
5 | import { setAge } from './actions/collectionActions02';
6 |
7 | import ContainerComponent from './containers/ContainerComponent';
8 | import PresentationComponent from './PresentationComponent';
9 | import DispatchContainer01 from './containers/DispatchContainer01';
10 |
11 | class AdvReduxApp extends PureComponent {
12 | store = configureStore({ loading: false });
13 |
14 | componentDidMount() {
15 | this.store.dispatch(
16 | setCollection([
17 | { id: 1, name: 'John', age: 20 },
18 | { id: 2, name: 'Park', age: 35 },
19 | ]),
20 | );
21 | this.store.dispatch(setAge(2, 55));
22 | const { collection } = this.store.getState();
23 | const { ids, entities } = collection;
24 | const originalPayload = ids.map(id => entities[id]);
25 | console.log(originalPayload);
26 | }
27 |
28 | render() {
29 | return (
30 |
31 | 화면 컴포넌트:
32 |
33 | 데이터 컴포넌트:
34 |
35 | 액션 데이터 컴포넌트:
36 |
37 | );
38 | }
39 | }
40 | export default AdvReduxApp;
41 |
--------------------------------------------------------------------------------
/src/stories/DoitUITableStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import Table from '../doit-ui/Table';
5 | import TableHead from '../doit-ui/TableHead';
6 | import TableBody from '../doit-ui/TableBody';
7 | import TableRow from '../doit-ui/TableRow';
8 | import TableCell from '../doit-ui/TableCell';
9 |
10 | storiesOf('Doit-UI/Table', module)
11 | .addWithJSX('Table 예제', () => (
12 |
13 |
14 |
15 | 코인
16 | 시가 총액
17 | 현재 시세
18 | 거래 시간
19 |
20 |
21 |
22 |
23 | 비트코인(BTX)
24 | 123,123,000,000원
25 | 4,200,000원
26 | 2019/01/20 08:23:22
27 |
28 |
29 | 두잇코인(DOIT)
30 | 3,123,000,000원
31 | 200,000원
32 | 2019/01/19 08:23:22
33 |
34 |
35 |
36 | ));
37 |
--------------------------------------------------------------------------------
/src/doit-ui/Modal/create.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import Modal from '../Modal'; // 혹은 import Modal from './index';
3 |
4 | import { Provider } from './context';
5 |
6 | export default function createModalProvider(ContentMap = {}) {
7 | return class ModalProvider extends PureComponent {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.state = { showModal: false };
12 | this.handleClose = this.handleClose.bind(this);
13 | this.handleOpen = this.handleOpen.bind(this);
14 | }
15 |
16 | handleOpen(contentId, modalProps) {
17 | this.contentId = contentId;
18 | this.modalProps = modalProps;
19 | this.setState({ showModal: true });
20 | }
21 |
22 | handleClose() {
23 | this.setState({ showModal: false });
24 | }
25 |
26 | render() {
27 | const { children } = this.props;
28 | const { showModal } = this.state;
29 | const ModalContent = ContentMap[this.contentId];
30 |
31 | return (
32 |
38 | {children}
39 | {showModal && ModalContent && (
40 |
41 |
42 |
43 | )}
44 |
45 | );
46 | }
47 | };
48 | }
49 |
--------------------------------------------------------------------------------
/src/stories/DoitUITextStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import Text from '../doit-ui/Text';
5 |
6 | storiesOf('Doit-UI/Text', module).addWithJSX('Text 예제', () => (
7 |
8 | Xlarge
9 | Xlarge primary
10 | Xlarge secondary
11 | Xlarge bold
12 | Xlarge light
13 |
14 | Large
15 | Large primary
16 | Large secondary
17 | Large bold
18 | Large light
19 |
20 | default
21 | default primary
22 | default secondary
23 | default bold
24 | default light
25 |
26 | small
27 | small primary
28 | small secondary
29 | Large bold
30 | Large light
31 |
32 | xsmall
33 | small primary
34 | small secondary
35 | Large bold
36 | Large light
37 |
38 |
39 | ));
40 |
--------------------------------------------------------------------------------
/src/stories/DoitUIInlineListStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import InlineList from '../doit-ui/InlineList';
4 | import { css } from '../doit-ui/withStyles';
5 |
6 | function RedBox({ children }) {
7 | return (
8 |
9 | {children}
10 |
11 | )
12 | }
13 |
14 | storiesOf('Doit-UI/InlineList', module)
15 | .addWithJSX('기본 설정', () => (
16 |
17 | 1
18 | 2
19 | 3
20 |
21 | ))
22 | .addWithJSX('align=center', () => (
23 |
24 | 1
25 | 2
26 | 3
27 |
28 | ))
29 | .addWithJSX('align=right', () => (
30 |
31 | 1
32 | 2
33 | 3
34 |
35 | ))
36 | .addWithJSX('verticalAlign=top', () => (
37 |
38 | 1
39 | 2
40 | 3
41 |
42 | ))
43 | .addWithJSX('verticalAlign=bottom', () => (
44 |
45 | 1
46 | 2
47 | 3
48 |
49 | ));
50 |
--------------------------------------------------------------------------------
/src/stories/ReduxStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import ReduxApp01 from '../07/ReduxApp01';
5 | import ReduxApp02 from '../07/ReduxApp02';
6 | import ReduxApp03 from '../07/ReduxApp03';
7 | import AdvReduxApp01 from '../07/AdvReduxApp01';
8 | import AdvReduxApp02 from '../07/AdvReduxApp02';
9 | import AdvReduxApp03 from '../07/AdvReduxApp03';
10 | import AdvReduxApp04 from '../07/AdvReduxApp04';
11 | import AdvReduxApp05 from '../07/AdvReduxApp05';
12 | import AdvReduxApp06 from '../07/AdvReduxApp06';
13 | import AdvReduxApp07 from '../07/AdvReduxApp07';
14 | import AdvReduxApp08 from '../07/AdvReduxApp08';
15 | import SearchFilterReduxApp from '../07/SearchFilterReduxApp';
16 |
17 | storiesOf('ReduxApp', module)
18 | .addWithJSX('기본 스토어 설정', () => )
19 | .addWithJSX('기본 액션 호출', () => )
20 | .addWithJSX('기본 리듀서 구현', () => )
21 | .addWithJSX('다중 리듀서 설정', () => )
22 | .addWithJSX('다중 액션 설정', () => )
23 | .addWithJSX('배열 액션 호출', () => )
24 | .addWithJSX('그래프DB 변환 작업', () => )
25 | .addWithJSX('그래프DB 수정 액션 호출', () => )
26 | .addWithJSX('데이터 컴포넌트 예제', () => )
27 | .addWithJSX('데이터 컴포넌트 액션 예제', () => )
28 | .addWithJSX('데이터 컴포넌트 전체 액션 예제', () => )
29 | .addWithJSX('검색 항목 예제', () => );
30 |
--------------------------------------------------------------------------------
/src/06/ButtonWithConsumer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from '../04/Button';
4 | import createLoadingConsumer from './createLoadingConsumer';
5 |
6 | const DefaultLoadingConsumer = createLoadingConsumer();
7 | const Loading2Consumer = createLoadingConsumer('loading2');
8 |
9 | function ButtonWithConsumer({ children }) {
10 | return (
11 |
12 | (
14 |
17 | )}
18 | />
19 | (
21 |
24 | )}
25 | />
26 | (
28 | (
30 |
33 | )}
34 | />
35 | )}
36 | />
37 |
38 | );
39 | }
40 |
41 | ButtonWithConsumer.propTypes = {
42 | children: PropTypes.string,
43 | };
44 |
45 | export default ButtonWithConsumer;
46 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Document, { Html, Main, Head, NextScript } from 'next/document';
3 | import { StyleSheetServer } from 'aphrodite';
4 |
5 | class MyDocument extends Document {
6 | static async getInitialProps({ renderPage }) {
7 | const { html, css } = StyleSheetServer.renderStatic(() => renderPage());
8 | const { renderedClassNames: ids } = css;
9 | return { ...html, css, ids };
10 | }
11 |
12 | constructor(props) {
13 | super(props);
14 | /* Take the renderedClassNames from aphrodite (as generated
15 | in getInitialProps) and assign them to __NEXT_DATA__ so that they
16 | are accessible to the client for rehydration. */
17 | const { __NEXT_DATA__, ids } = props;
18 | if (ids) {
19 | __NEXT_DATA__.ids = this.props.ids;
20 | }
21 | }
22 |
23 | render() {
24 | return (
25 |
26 |
27 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 | }
47 |
48 | export default MyDocument;
49 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import App, { Container } from 'next/app';
3 | import configureStore from '../src/08/store/configureStore';
4 | import { Provider } from 'react-redux';
5 | import ModalProvider from '../src/08/ModalProvider';
6 | import AppLayout from '../src/08/components/AppLayout';
7 | import NotificationContainer from '../src/08/containers/NotificationContainer';
8 | import { StaticRouter } from 'react-router';
9 | import { BrowserRouter } from 'react-router-dom';
10 | import RouterStateContainer from '../src/08/containers/RouterStateContainer';
11 |
12 | const isServer = typeof window === 'undefined';
13 |
14 | class MyApp extends App {
15 | store = configureStore();
16 |
17 | render() {
18 | const { Component, router, pageProps } = this.props;
19 | const Router = isServer ? StaticRouter : BrowserRouter;
20 | const routerProps = isServer ? { location: router.asPath } : {};
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export default MyApp;
42 |
--------------------------------------------------------------------------------
/mock/create.js:
--------------------------------------------------------------------------------
1 | const getRandomNumber = (min, max) => {
2 | const range = max - min + 1;
3 | return parseInt(Math.random() * range + min, 10);
4 | }
5 |
6 | const createData = (index) => {
7 | switch(getRandomNumber(0, 2)) {
8 | case 1: {
9 | return {
10 | id: `DOIT${index}`,
11 | code: 'DOIT',
12 | name: '두잇코인',
13 | totalPrice: getRandomNumber(10000000, 800000000),
14 | currentPrice: getRandomNumber(20000, 25000),
15 | amount: getRandomNumber(1, 30),
16 | datetime: '2019/01/20 08:23:22',
17 | };
18 | }
19 | case 2: {
20 | return {
21 | id: `ETH${index}`,
22 | code: 'ETH',
23 | name: '이더리움',
24 | totalPrice: getRandomNumber(100000000, 500000000),
25 | currentPrice: getRandomNumber(400000, 600000),
26 | amount: getRandomNumber(100, 1000),
27 | datetime: '2019/01/20 08:23:22',
28 | };
29 | }
30 | default: {
31 | return {
32 | id: `BTX${index}`,
33 | code: 'BTX',
34 | name: '비트코인',
35 | totalPrice: getRandomNumber(100000000000, 200000000000),
36 | currentPrice: getRandomNumber(10000000, 40000000),
37 | amount: getRandomNumber(0, 10),
38 | datetime: '2019/01/20 08:23:22',
39 | };
40 | }
41 | }
42 | }
43 |
44 | module.exports = function() {
45 | return {
46 | users: [],
47 | transactions: Array(100).fill('').map((_, index) => createData(index)),
48 | };
49 | };
50 |
--------------------------------------------------------------------------------
/src/11/api-redux-pack/createActions.js:
--------------------------------------------------------------------------------
1 | import { FETCH_LIST, CREATE, UPDATE, DELETE, FETCH, RESET } from './actionTypes';
2 | import Api from '../../08/Api';
3 |
4 | export default (resourceName, key = 'id') => ({
5 | collection: (params = {}, meta = {}) => ({
6 | type: FETCH_LIST,
7 | promise: Api.get(resourceName, { params }),
8 | meta: {
9 | ...meta,
10 | key,
11 | resourceName,
12 | },
13 | }),
14 | member: (id, params = {}, meta = {}) => ({
15 | type: FETCH,
16 | promise: Api.get(`${resourceName}/${id}`, { params }),
17 | meta: {
18 | ...meta,
19 | key,
20 | resourceName,
21 | },
22 | }),
23 | create: (data, params = {}, meta = {}) => ({
24 | type: CREATE,
25 | promise: Api.post(resourceName, data, { params }),
26 | meta: {
27 | ...meta,
28 | key,
29 | resourceName,
30 | }
31 | }),
32 | update: (id, data, params = {}, meta = {}) => ({
33 | type: UPDATE,
34 | promise: Api.put(`${resourceName}/${id}`, data, { params }),
35 | meta: {
36 | ...meta,
37 | key,
38 | resourceName,
39 | },
40 | }),
41 | destroy: (id, params = {}, meta = {}) => ({
42 | type: DELETE,
43 | promise: Api.delete(`${resourceName}/${id}`, { params }),
44 | meta: {
45 | ...meta,
46 | key,
47 | resourceName,
48 | },
49 | payload: { id },
50 | }),
51 | reset: () => ({
52 | type: RESET,
53 | meta: { resourceName },
54 | }),
55 | });
56 |
--------------------------------------------------------------------------------
/src/06/ModalProvider.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import Modal from './Modal';
3 | import Button from '../04/Button';
4 | import Text from '../04/Text';
5 |
6 | const { Provider, Consumer } = React.createContext({});
7 |
8 | export { Consumer };
9 |
10 | class ModalProvider extends PureComponent {
11 | constructor(props) {
12 | super(props);
13 |
14 | this.state = { showModal: false };
15 | this.handleClose = this.handleClose.bind(this);
16 | this.handleOpen = this.handleOpen.bind(this);
17 | }
18 |
19 | handleOpen(contentId, modalProps) {
20 | this.contentId = contentId;
21 | this.modalProps = modalProps;
22 | this.setState({ showModal: true });
23 | }
24 |
25 | handleClose() {
26 | this.setState({ showModal: false });
27 | }
28 |
29 | render() {
30 | const { children } = this.props;
31 | const { showModal } = this.state;
32 |
33 | return (
34 |
40 | {children}
41 | {showModal && (
42 |
43 |
44 | 정말로 삭제 하시겠습니까?
45 |
46 |
47 |
48 |
49 | )}
50 |
51 | );
52 | }
53 | }
54 |
55 | export default ModalProvider;
56 |
--------------------------------------------------------------------------------
/src/02/02-4.js:
--------------------------------------------------------------------------------
1 | const num = 1;
2 | num = 3; // 타입 에러가 발생합니다
3 |
4 | const str = '문자';
5 | str = '새 문자'; // 타입 에러가 발생합니다
6 |
7 | const arr = [];
8 | arr = [1, 2, 3]; // 타입 에러가 발생합니다
9 |
10 | const obj = {};
11 | obj = { name: '내 이름' }; // 타입 에러가 발생합니다
12 |
13 | const arr2 = [];
14 | arr2.push(1); // arr2 = [1]
15 | arr2.splice(0, 0, 0); // arr2 = [0,1]
16 | arr2.pop(); // arr2 = [1]
17 |
18 | const obj2 = {};
19 | obj2['name'] = '내이름'; // obj2 = { name: '내이름' }
20 | Object.assign(obj2, { name: '새이름' }); //obj2 = { name: '새이름' }
21 | delete obj2.name; //obj2 = {}
22 |
23 | const num1 = 1;
24 | const num2 = num1 * 3; // num2 = 3
25 |
26 | const str1 = '문자';
27 | const str2 = str1 + '추가'; // str2 = '문자추가'
28 |
29 | const arr3 = [];
30 | const arr4 = arr3.concat(1); // arr4 = [1]
31 | const arr5 = [...arr4, 2, 3]; // arr5 = [1, 2, 3]
32 | const arr6 = arr5.slice(0, 1); // arr6 = [1], arr5 = [1, 2, 3]
33 | const [first, ...arr7] = arr5; // arr7 = [2, 3], first = 1
34 |
35 | const obj3 = { name: '내이름', age: 20 };
36 | const obj4 = { ...obj3, name: '새이름' }; // obj4 = { name: '새이름', age: 20}
37 | const { name, ...obj5 } = obj4; // obj5 = { age: 20 }
38 |
39 | const arr = [1, 2, 3];
40 | // 가변 변수를 사용한 예
41 | for (let i = 0; i < arr.length; i++) {
42 | console.log(arr[i]);
43 | }
44 | // iterator 방식의 for-in 루프와 함께 불변 변수를 사용한 예
45 | for (const item in arr) {
46 | console.log(item);
47 | }
48 |
49 | // forEach 함수를 활용한 예
50 | arr.forEach((item, index) => {
51 | console.log(item);
52 | console.log(index);
53 | });
54 |
--------------------------------------------------------------------------------
/src/06/HomePageComponent.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import ButtonWithContext from './ButtonWithContext';
4 | import Button from '../04/Button';
5 |
6 | function RowBComponent() {
7 | return ;
8 | }
9 |
10 | function RowCComponent() {
11 | return 버튼;
12 | }
13 |
14 | function TableComponent() {
15 | return (
16 |
20 | );
21 | }
22 |
23 | class HomePageComponent extends PureComponent {
24 | constructor(props) {
25 | super(props);
26 |
27 | this.state = { loading: false };
28 | this.setLoading = this.setLoading.bind(this);
29 | this.toggleLoading = this.toggleLoading.bind(this);
30 | }
31 |
32 | getChildContext() {
33 | return {
34 | loading: this.state.loading,
35 | setLoading: this.setLoading,
36 | };
37 | }
38 |
39 | setLoading(loading) {
40 | this.setState({ loading });
41 | }
42 |
43 | toggleLoading() {
44 | this.setState(({ loading }) => ({ loading: !loading }));
45 | }
46 |
47 | render() {
48 | return (
49 |
53 | );
54 | }
55 | }
56 |
57 | HomePageComponent.childContextTypes = {
58 | loading: PropTypes.bool,
59 | setLoading: PropTypes.func,
60 | };
61 |
62 | export default HomePageComponent;
63 |
--------------------------------------------------------------------------------
/src/07/SearchFilterReduxApp.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Provider } from 'react-redux';
3 | import configureStore from './configureStore';
4 | import ContainerComponent from './containers/ContainerComponent';
5 | import PresentationComponent from './PresentationComponent';
6 | import DispatchContainer from './containers/DispatchContainer03';
7 | import SearchFilterInputContainer from './containers/SearchFilterInputContainer';
8 | import SearchResetButtonContainer from './containers/SearchResetButtonContainer';
9 | import SearchResultTableContainer from './containers/SearchResultTableContainer';
10 |
11 | class SearchFilterReduxApp extends PureComponent {
12 | store = configureStore({ loading: false });
13 |
14 | render() {
15 | return (
16 |
17 | 리덕스 예제
18 |
24 |
25 | 화면 컴포넌트:
26 |
27 | 데이터용 컴포넌트:
28 |
29 |
30 |
31 | );
32 | }
33 | }
34 | export default SearchFilterReduxApp;
35 |
--------------------------------------------------------------------------------
/src/02/CoinApp.js:
--------------------------------------------------------------------------------
1 | import throttle from './throttle';
2 |
3 | const MICRO = 100000;
4 |
5 | class CoinApp {
6 | constructor(coins) {
7 | this.coins = Object.entries(coins).reduce((coins, [code, amount]) => ({
8 | ...coins,
9 | [code]: { microAmount: amount * MICRO },
10 | }), {});
11 | this.sell = throttle(this.sell.bind(this), 1000);
12 | }
13 |
14 | buy(code, price, amount) {
15 | const microAmount = amount * MICRO;
16 | if (!this.coins[code]) return;
17 |
18 | const {
19 | microAmount: currentAmount,
20 | price: currentPrice,
21 | } = this.coins[code];
22 |
23 | if (microAmount > currentAmount) return;
24 | if (currentPrice && price < currentPrice) return;
25 |
26 | this.coins[code] = {
27 | microAmount: currentAmount - microAmount,
28 | price,
29 | };
30 | }
31 |
32 | sell(code, price, amount) {
33 | const microAmount = amount * MICRO;
34 |
35 | const {
36 | microAmount: currentAmount = 0,
37 | price: currentPrice,
38 | } = this.coins[code] || {};
39 |
40 | if (currentPrice && price > currentPrice) return;
41 |
42 | this.coins[code] = {
43 | microAmount: currentAmount + microAmount,
44 | price,
45 | };
46 | }
47 |
48 | print() {
49 | console.log('=====현재 거래소 보유 현황======');
50 | console.log('코드|현재 가격|시가 총액');
51 | Object.entries(this.coins)
52 | .forEach(([code, { microAmount, price }]) =>
53 | console.log(`${code}|${price}|${microAmount * price / MICRO}`)
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/stories/DoitUIButtonStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import Button from '../doit-ui/Button';
5 | import Text from '../doit-ui/Text';
6 |
7 | storiesOf('Doit-UI/Button', module)
8 | .addWithJSX('모두 보기', () => (
9 |
10 |
11 |
12 | 기본 버튼
13 |
14 |
15 |
16 | xlarge 버튼
17 |
18 |
19 |
20 | large 버튼
21 |
22 |
23 |
24 | small 버튼
25 |
26 |
27 |
28 | xsmall 버튼
29 |
30 |
31 |
32 | primary 버튼
33 |
34 |
35 |
36 | secondary 버튼
37 |
38 |
39 | ))
40 | .addWithJSX('large 예제', () => )
41 | .addWithJSX('xlarge 예제', () => )
42 | .addWithJSX('small 예제', () => )
43 | .addWithJSX('xsmall 예제', () => )
44 | .addWithJSX('primary 예제', () => )
45 | .addWithJSX('secondary 예제', () => )
46 | .addWithJSX('primary와 large 함께 쓰는 예제', () => );
47 |
--------------------------------------------------------------------------------
/src/08/components/main/TransactionPagination.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from '../../../doit-ui/Button';
4 | import InlineList from '../../../doit-ui/InlineList';
5 |
6 | class TransactionPagination extends PureComponent {
7 | constructor(props) {
8 | super(props);
9 | this.handleNextPress = this.handleNextPress.bind(this);
10 | this.handlePrevPress = this.handlePrevPress.bind(this);
11 | }
12 | handleNextPress() {
13 | const { requestTransactionList, searchParams, pageNumber } = this.props;
14 | requestTransactionList(searchParams, pageNumber + 1);
15 | }
16 | handlePrevPress() {
17 | const { requestTransactionList, searchParams, pageNumber } = this.props;
18 | requestTransactionList(searchParams, pageNumber - 1);
19 | }
20 | render() {
21 | const { loading, pageNumber, hasNext } = this.props;
22 | const prevDisabled = loading || pageNumber <= 1;
23 | const nextDisabled = loading || !hasNext;
24 | return (
25 |
26 |
29 |
32 |
33 | );
34 | }
35 | }
36 |
37 | TransactionPagination.propTypes = {
38 | hasNext: PropTypes.bool,
39 | pageNumber: PropTypes.number,
40 | loading: PropTypes.bool,
41 | requestTransactionList: PropTypes.func.isRequired,
42 | };
43 |
44 | export default TransactionPagination;
45 |
--------------------------------------------------------------------------------
/src/08/CoinApp.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Provider } from 'react-redux';
3 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
4 |
5 | import AppLayout from './components/AppLayout';
6 | import MainPage from './components/main/MainPage';
7 | import NotFound from './components/NotFound';
8 | // import CoinOverview from './components/main/CoinOverview';
9 | // import TransactionListContainer from './containers/main/TransactionListContainer';
10 | import configureStore from './store/configureStore';
11 | import ModalProvider from './ModalProvider';
12 | // import NotificationContainer from './containers/main/NotificationContainer';
13 | import NotificationContainer from './containers/NotificationContainer';
14 | // import RegisterPageContainer from './containers/signup/RegisterPageContainer';
15 | import RouterStateContainer from './containers/RouterStateContainer';
16 | // import MainPage from '../13/AsyncMainPage';
17 |
18 | class CoinApp extends PureComponent {
19 | store = configureStore();
20 |
21 | render() {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 | } />
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export default CoinApp;
42 |
--------------------------------------------------------------------------------
/src/doit-ui/TableCell.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { withStyles, css, withStylesPropTypes } from './withStyles';
4 |
5 | class TableCell extends PureComponent {
6 | render() {
7 | const { align, baseline, styles, children, isHeader } = this.props;
8 | const Tag = isHeader ? 'th' : 'td';
9 | return (
10 |
19 | {children}
20 |
21 | );
22 | }
23 | }
24 |
25 | TableCell.propTypes = {
26 | ...withStylesPropTypes,
27 | align: PropTypes.oneOf(['left', 'center', 'right']),
28 | baseline: PropTypes.bool,
29 | children: PropTypes.node,
30 | isHeader: PropTypes.bool,
31 | };
32 |
33 | TableCell.defaultProps = {
34 | baseline: true,
35 | isHeader: false,
36 | };
37 |
38 | export default withStyles(({ color, unit }) => ({
39 | cell: {
40 | paddingTop: unit * 4,
41 | paddingBottom: unit * 4,
42 | paddingRight: unit * 8,
43 | paddingLeft: unit * 8,
44 | backgroundColor: color.white,
45 | textAlign: 'left'
46 | },
47 | header: {
48 | backgroundColor: color.primary,
49 | color: color.white
50 | },
51 | baseline: {
52 | borderBottom: `1px solid ${color.border}`
53 | },
54 | alignCenter: {
55 | textAlign: 'center',
56 | },
57 | alignRight: {
58 | textAlign: 'right',
59 | },
60 | }))(TableCell);
61 |
--------------------------------------------------------------------------------
/src/08/actions/transactionActions.js:
--------------------------------------------------------------------------------
1 | import Api from '../Api';
2 | import { showMessage } from './notificationActions';
3 |
4 | export const LOADING_TRANSACTION_LIST = 'transaction/LOADING_TRANSACTION_LIST';
5 | export const SET_TRANSACTION_LIST = 'transaction/SET_TRANSACTION_LIST';
6 | export const SET_ERROR = 'transaction/SET_ERROR';
7 | export const TRADE_COMPLETE = 'transaction/TRADE_COMPLETE';
8 |
9 | export function loading() {
10 | return {
11 | type: LOADING_TRANSACTION_LIST,
12 | };
13 | }
14 |
15 | export function setError(errorMessage) {
16 | return {
17 | type: SET_ERROR,
18 | payload: { errorMessage },
19 | };
20 | }
21 |
22 | export function setTransactionList(transactions) {
23 | return {
24 | type: SET_TRANSACTION_LIST,
25 | payload: transactions,
26 | };
27 | }
28 |
29 | export function requestTransactionList(params) {
30 | return dispatch => {
31 | dispatch(loading());
32 | Api.get('/transactions', { params }).then(
33 | ({ data }) => dispatch(setTransactionList(data)),
34 | error => {
35 | dispatch(setError(error.response.data.errorMessage));
36 | // dispatch(showMessage(error.response.data.errorMessage, true));
37 | },
38 | );
39 | };
40 | }
41 |
42 | export function tradeComplete() {
43 | return { type: TRADE_COMPLETE };
44 | }
45 |
46 | export function createTransaction(data, onComplete) {
47 | return dispatch =>
48 | Api.post('/transactions', data).then(
49 | ({ data }) => {
50 | dispatch(tradeComplete());
51 | onComplete();
52 | },
53 | error => dispatch(setError(error.response.data.errorMessage)),
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/02/02-6.js:
--------------------------------------------------------------------------------
1 | // ES5 문법
2 | function Shape(x, y) {
3 | this.name = 'Shape';
4 | this.move(x, y);
5 | }
6 | // static 타입 선언 예제
7 | Shape.create = function(x, y) {
8 | return new Shape(x, y);
9 | };
10 | Shape.prototype.move = function(x, y) {
11 | this.x = x;
12 | this.y = y;
13 | };
14 | Shape.prototype.area = function() {
15 | return 0;
16 | };
17 |
18 | // 혹은
19 | Shape.prototype = {
20 | move: function(x, y) {
21 | this.x = x;
22 | this.y = y;
23 | },
24 | area: function() {
25 | return 0;
26 | },
27 | };
28 |
29 | var s = new Shape(0, 0);
30 | var s2 = Shape.create(0, 0);
31 | s.area(); // 0
32 |
33 | function Circle(x, y, radius) {
34 | Shape.call(this, x, y);
35 | this.name = 'Circle';
36 | this.radius = radius;
37 | }
38 | Object.assign(Circle.prototype, Shape.prototype, {
39 | area: function() {
40 | return this.radius * this.radius;
41 | },
42 | });
43 |
44 | var c = new Circle(0, 0, 10);
45 | c.area(); // 100
46 |
47 | // ES6 예제
48 | class Shape {
49 | static create(x, y) {
50 | return new Shape(x, y);
51 | }
52 | name = 'Shape';
53 |
54 | constructor(x, y) {
55 | this.move(x, y);
56 | }
57 | move(x, y) {
58 | this.x = x;
59 | this.y = y;
60 | }
61 | area() {
62 | return 0;
63 | }
64 | }
65 |
66 | var s = new Shape(0, 0);
67 | var s1 = Shape.create(0, 0);
68 | s.area(); // 0
69 |
70 | class Circle extends Shape {
71 | constructor(x, y, radius) {
72 | super(x, y);
73 | this.radius = radius;
74 | }
75 | area() {
76 | if (this.radius === 0) return super.area();
77 | return this.radius * this.radius;
78 | }
79 | }
80 |
81 | var c = new Circle(0, 0, 10);
82 | c.area(); // 100
83 |
--------------------------------------------------------------------------------
/src/04/Text.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import withStyles, { css } from './withStyles';
4 |
5 | class Text extends PureComponent {
6 | render() {
7 | const {
8 | children,
9 | styles,
10 | large,
11 | xlarge,
12 | small,
13 | xsmall,
14 | primary,
15 | secondary,
16 | } = this.props;
17 | return (
18 |
29 | {children}
30 |
31 | );
32 | }
33 | }
34 |
35 | Text.propTypes = {
36 | children: PropTypes.node.isRequired,
37 | xsmall: PropTypes.bool,
38 | small: PropTypes.bool,
39 | large: PropTypes.bool,
40 | xlarge: PropTypes.bool,
41 | secondary: PropTypes.bool,
42 | primary: PropTypes.bool,
43 | };
44 |
45 | export default withStyles(({ color, size, responsive }) => ({
46 | default: {
47 | color: color.default,
48 | fontSize: size.md,
49 | [responsive.small]: {
50 | textAlign: 'center',
51 | width: '100%',
52 | },
53 | },
54 | xlarge: {
55 | fontSize: size.xg,
56 | },
57 | large: {
58 | fontSize: size.lg,
59 | },
60 | small: {
61 | fontSize: size.sm,
62 | },
63 | xsmall: {
64 | fontSize: size.xs,
65 | },
66 | primary: {
67 | color: color.primary,
68 | },
69 | secondary: {
70 | color: color.secondary,
71 | },
72 | }))(Text);
73 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
21 | React App
22 |
23 |
24 |
25 |
26 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/08/components/AppNav.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { withStyles, css, withStylesPropTypes } from '../../doit-ui/withStyles';
3 | import Heading from '../../doit-ui/Heading';
4 | // import Text from '../../doit-ui/Text';
5 | import Button from '../../doit-ui/Button';
6 | import { Consumer as Modal } from '../../doit-ui/Modal/context';
7 | import { REGISTER_USER_MODAL } from '../constants/modals';
8 |
9 | export const HEIGHT = 64;
10 |
11 | class AppNav extends PureComponent {
12 | render() {
13 | const { styles } = this.props;
14 | return (
15 |
16 |
17 |
18 | 두잇 코인 거래소
19 |
20 |
21 | {({ openModal }) => (
22 |
25 | )}
26 |
27 |
28 |
29 | );
30 | }
31 | }
32 |
33 | AppNav.propTypes = {
34 | ...withStylesPropTypes,
35 | };
36 |
37 | export default withStyles(({ color, depth, unit }) => ({
38 | wrapper: {
39 | ...depth.level1,
40 | display: 'flex',
41 | flexDirection: 'column',
42 | justifyContent: 'center',
43 | position: 'fixed',
44 | top: 0,
45 | left: 0,
46 | width: '100%',
47 | height: HEIGHT - 4,
48 | backgroundColor: color.primary,
49 | },
50 | container: {
51 | display: 'flex',
52 | justifyContent: 'space-between',
53 | alignItems: 'center',
54 | paddingLeft: unit * 2,
55 | paddingRight: unit * 2,
56 | },
57 | }))(AppNav);
58 |
--------------------------------------------------------------------------------
/src/08/components/main/CoinDashlet.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Heading from '../../../doit-ui/Heading';
4 | import Button from '../../../doit-ui/Button';
5 | import Card from '../../../doit-ui/Card';
6 | import InlineList from '../../../doit-ui/InlineList';
7 | import Text from '../../../doit-ui/Text';
8 | import { Consumer as Modal } from '../../../doit-ui/Modal/context';
9 | import { TRADE_COIN_MODAL } from '../../constants/modals';
10 |
11 | class CoinDashlet extends PureComponent {
12 | render() {
13 | const { name, priceLabel } = this.props;
14 | return (
15 |
16 | {({ openModal }) => (
17 |
18 |
19 | {name}
20 | {priceLabel}
21 |
22 |
23 |
32 |
40 |
41 |
42 | )}
43 |
44 | );
45 | }
46 | }
47 | CoinDashlet.propTypes = {
48 | name: PropTypes.string,
49 | priceLabel: PropTypes.string,
50 | };
51 | export default CoinDashlet;
52 |
--------------------------------------------------------------------------------
/src/stories/ButtonStory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import Button from '../04/Button';
5 | import Text from '../04/Text';
6 |
7 | storiesOf('Button', module)
8 | // .addWithJSX('모두 보기', () => (
9 | //
10 | //
11 | //
12 | // 기본 버튼
13 | //
14 | //
15 | //
16 | // xlarge 버튼
17 | //
18 | //
19 | //
20 | // large 버튼
21 | //
22 | //
23 | //
24 | // small 버튼
25 | //
26 | //
27 | //
28 | // xsmall 버튼
29 | //
30 | //
31 | //
32 | // primary 버튼
33 | //
34 | //
35 | //
36 | // secondary 버튼
37 | //
38 | //
39 | // ))
40 | .addWithJSX('기본 설정', () => )
41 | .addWithJSX('large 예제', () => )
42 | .addWithJSX('xlarge 예제', () => )
43 | .addWithJSX('small 예제', () => )
44 | .addWithJSX('xsmall 예제', () => )
45 | .addWithJSX('primary 예제', () => )
46 | .addWithJSX('secondary 예제', () => )
47 | .addWithJSX('primary와 large 함께 쓰는 예제', () => (
48 |
51 | ));
52 |
--------------------------------------------------------------------------------
/src/04/CheckBox.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import withStyles, { css } from './withStyles';
4 |
5 | class CheckBox extends PureComponent {
6 | constructor(props) {
7 | super(props);
8 | this.setRef = this.setRef.bind(this);
9 | this.handleClick = this.handleClick.bind(this);
10 | }
11 | componentDidMount() {
12 | if (this.props.autoFocus) {
13 | this.ref.focus();
14 | }
15 | }
16 | handleClick(e) {
17 | const { name, onChange } = this.props;
18 | onChange(name, e.target.checked);
19 | }
20 | setRef(ref) {
21 | this.ref = ref;
22 | }
23 | render() {
24 | const {
25 | errorMessage,
26 | label,
27 | children,
28 | styles,
29 | checked,
30 | } = this.props;
31 | return (
32 |
49 | );
50 | }
51 | }
52 |
53 | CheckBox.propTypes = {
54 | name: PropTypes.string.isRequired,
55 | autoFocus: PropTypes.bool,
56 | checked: PropTypes.bool,
57 | onChange: PropTypes.func,
58 | };
59 | CheckBox.defaultProps = {
60 | autoFocus: false,
61 | checked: false,
62 | onChange: () => {},
63 | };
64 |
65 | export default withStyles(({ color, size }) => ({
66 | errorText: {
67 | fontSize: size.sm,
68 | color: color.error,
69 | },
70 | }))(CheckBox);
71 |
--------------------------------------------------------------------------------
/src/03/Input.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class Input extends PureComponent {
5 | constructor(props) {
6 | super(props);
7 | this.setRef = this.setRef.bind(this);
8 | this.handleChange = this.handleChange.bind(this);
9 | }
10 | handleChange(e) {
11 | const { name, onChange } = this.props;
12 | if (onChange) {
13 | onChange(name, e.target.value)
14 | }
15 | }
16 | componentDidMount() {
17 | if (this.props.autoFocus) {
18 | this.ref.focus();
19 | }
20 | }
21 | componentDidUpdate() {
22 | if (this.props.autoFocus) {
23 | this.ref.focus();
24 | }
25 | }
26 | setRef(ref) {
27 | this.ref = ref;
28 | }
29 | render() {
30 | const { errorMessage, label, name, value, type, onFocus } = this.props;
31 | return (
32 |
44 | );
45 | }
46 | }
47 |
48 | Input.propTypes = {
49 | type: PropTypes.oneOf(['text', 'number', 'price']),
50 | name: PropTypes.string.isRequired,
51 | value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
52 | errorMessage: PropTypes.string,
53 | label: PropTypes.string,
54 | onChange: PropTypes.func,
55 | onFocus: PropTypes.func,
56 | autoFocus: PropTypes.bool,
57 | };
58 | Input.defaultProps = {
59 | onChange: () => {},
60 | onFocus: () => {},
61 | autoFocus: false,
62 | type: 'text',
63 | };
64 |
65 | export default Input;
66 |
--------------------------------------------------------------------------------
/src/doit-ui/Theme.js:
--------------------------------------------------------------------------------
1 | export const LARGE_AND_ABOVE = 'largeAndAbove';
2 | const BREAKPOINT_NAMES = {
3 | LARGE: 'large',
4 | MEDIUM: 'medium',
5 | SMALL: 'small',
6 | };
7 |
8 | const breakpoints = {
9 | [BREAKPOINT_NAMES.LARGE]: 1128,
10 | [BREAKPOINT_NAMES.MEDIUM]: 744,
11 | [BREAKPOINT_NAMES.SMALL]: 327,
12 | };
13 |
14 | const responsive = {
15 | [LARGE_AND_ABOVE]: `@media (min-width: ${breakpoints[BREAKPOINT_NAMES.LARGE]}px)`,
16 | [BREAKPOINT_NAMES.SMALL]: `@media (max-width: ${breakpoints[BREAKPOINT_NAMES.MEDIUM] - 1}px)`,
17 | print: '@media print',
18 | };
19 | export const unit = 4;
20 |
21 | export default {
22 | // 색상
23 | color: {
24 | primary: '#2196F3', // 주 색상
25 | primaryDark: '#1976D2',
26 | secondary: '#009688', // 부 색상
27 | secondaryDark: '#00796b',
28 | white: '#FFFFFF',
29 | gray: '#9e9e9e',
30 | grayLight: '#eeeeee',
31 | grayDark: '#616161',
32 | border: 'rgba(0, 0, 0, .15)',
33 | default: '#333333', // 기본 문자 색상
34 | error: '#e51c23', // 오류 색상
35 | },
36 | // 폰트 사이즈
37 | size: {
38 | h1: 48,
39 | h2: 36,
40 | h3: 28,
41 | xg: 24,
42 | lg: 18,
43 | md: 14,
44 | sm: 12,
45 | xs: 10,
46 | },
47 | lineHeight: {
48 | xg: 1.6,
49 | lg: 1.6,
50 | md: 1.5,
51 | sm: 1.4,
52 | xs: 1.4,
53 | },
54 | fontWeight: {
55 | bold: 'bold',
56 | normal: 'normal',
57 | light: 300,
58 | },
59 | depth: {
60 | level1: {
61 | boxShadow: '0 2px 2px 0 rgba(0, 0, 0, 0.14)',
62 | },
63 | level2: {
64 | boxShadow: '0 4px 5px 0 rgba(0, 0, 0, 0.14)',
65 | },
66 | level3: {
67 | boxShadow: '0 8px 17px 12px rgba(0, 0, 0, 0.14)',
68 | },
69 | },
70 | // 길이 단위
71 | unit,
72 | // 반응형 미디어 속성
73 | responsive,
74 | };
75 |
--------------------------------------------------------------------------------
/src/08/middlewares/notificationEffects.js:
--------------------------------------------------------------------------------
1 | // import { SET_ERROR } from '../actions/transactionActions';
2 | import { KEY, LIFECYCLE } from 'redux-pack';
3 | // import { FETCH_TRANSACTION_LIST } from '../actions/transactionPackActions';
4 | import { SHOW_NOTIFICATION, showMessage, hideMessage } from '../actions/notificationActions';
5 | import { debounce } from '../../02/debounce';
6 |
7 | const debounceRunner = debounce(action => action(), 4000);
8 |
9 | // let prevHideCaller;
10 | export default store => nextRunner => action => {
11 | const { type, payload, meta } = action;
12 | const result = nextRunner(action);
13 | if (meta && meta.notification) {
14 | const { success, error } = meta.notification;
15 | if (success && meta[KEY.LIFECYCLE] === LIFECYCLE.SUCCESS) {
16 | store.dispatch(showMessage(success));
17 | } else if (error && meta[KEY.LIFECYCLE] === LIFECYCLE.FAILURE) {
18 | const { errorMessage } = payload.response ? payload.response.data : {};
19 | store.dispatch(showMessage(errorMessage || error, true));
20 | }
21 | // } else if (type === SET_ERROR) {
22 | // const { errorMessage } = payload;
23 | // store.dispatch(showMessage(errorMessage, true));
24 | // } else if (type === FETCH_TRANSACTION_LIST && meta[KEY.LIFECYCLE] === LIFECYCLE.FAILURE) {
25 | // const { errorMessage } = payload.response.data;
26 | // store.dispatch(showMessage(errorMessage, true));
27 | // } else if (type === FETCH_TRANSACTION_LIST && meta[KEY.LIFECYCLE] === LIFECYCLE.SUCCESS) {
28 | // const message = '거래 목록을 최신 정보로 업데이트하였습니다.';
29 | // store.dispatch(showMessage(message));
30 | } else if (type === SHOW_NOTIFICATION) {
31 | const hide = () => store.dispatch(hideMessage());
32 | // setTimeout(hide, 4000);
33 | debounceRunner(hide);
34 | }
35 | return result;
36 | };
37 |
--------------------------------------------------------------------------------
/src/sass/components/_modal.scss:
--------------------------------------------------------------------------------
1 | .modal {
2 | &:focus {
3 | outline: none;
4 | }
5 |
6 | @extend .z-depth-5;
7 |
8 | display: none;
9 | position: fixed;
10 | left: 0;
11 | right: 0;
12 | background-color: #fafafa;
13 | padding: 0;
14 | max-height: 70%;
15 | width: 55%;
16 | margin: auto;
17 | overflow-y: auto;
18 |
19 | border-radius: 2px;
20 | will-change: top, opacity;
21 |
22 | @media #{$medium-and-down} {
23 | width: 80%;
24 | }
25 |
26 | h1,h2,h3,h4 {
27 | margin-top: 0;
28 | }
29 |
30 | .modal-content {
31 | padding: 24px;
32 | }
33 | .modal-close {
34 | cursor: pointer;
35 | }
36 |
37 | .modal-footer {
38 | border-radius: 0 0 2px 2px;
39 | background-color: #fafafa;
40 | padding: 4px 6px;
41 | height: 56px;
42 | width: 100%;
43 | text-align: right;
44 |
45 | .btn, .btn-flat {
46 | margin: 6px 0;
47 | }
48 | }
49 | }
50 | .modal-overlay {
51 | position: fixed;
52 | z-index: 999;
53 | top: -25%;
54 | left: 0;
55 | bottom: 0;
56 | right: 0;
57 | height: 125%;
58 | width: 100%;
59 | background: #000;
60 | display: none;
61 |
62 | will-change: opacity;
63 | }
64 |
65 | // Modal with fixed action footer
66 | .modal.modal-fixed-footer {
67 | padding: 0;
68 | height: 70%;
69 |
70 | .modal-content {
71 | position: absolute;
72 | height: calc(100% - 56px);
73 | max-height: 100%;
74 | width: 100%;
75 | overflow-y: auto;
76 | }
77 |
78 | .modal-footer {
79 | border-top: 1px solid rgba(0,0,0,.1);
80 | position: absolute;
81 | bottom: 0;
82 | }
83 | }
84 |
85 | // Modal Bottom Sheet Style
86 | .modal.bottom-sheet {
87 | top: auto;
88 | bottom: -100%;
89 | margin: 0;
90 | width: 100%;
91 | max-height: 45%;
92 | border-radius: 0;
93 | will-change: bottom, opacity;
94 | }
95 |
--------------------------------------------------------------------------------