├── functions ├── .gitignore ├── index.js └── package.json ├── .env ├── next.config.js ├── .firebaserc ├── public ├── favicon.ico └── index.html ├── src ├── 11 │ └── api-redux-pack │ │ ├── actionTypes.js │ │ └── createActions.js ├── 13 │ ├── AsyncMainPage.jsx │ ├── AsyncRegisterPage.jsx │ └── AsyncComponent.jsx ├── styles.scss ├── 02 │ ├── MyModule.js │ ├── quiz │ ├── debounce.js │ ├── 02-8.js │ ├── 02-7.js │ ├── 02-10.js │ ├── 02-7-2.js │ ├── 02-2.js │ ├── 02-3-2.js │ ├── 02-5.js │ ├── throttle.js │ ├── 02-3.js │ ├── 02-4.js │ ├── CoinApp.js │ └── 02-6.js ├── 06 │ ├── FormContext.jsx │ ├── ModalContext.js │ ├── ButtonWithContext.jsx │ ├── ModalProviderWithKey.jsx │ ├── NewModalContent.jsx │ ├── createLoadingConsumer.jsx │ ├── DeleteModalContent.jsx │ ├── ButtonWithLoadingContext.jsx │ ├── FormSubmitButton.jsx │ ├── FormConsumerExample.jsx │ ├── LoadingProviderWithNewContext.jsx │ ├── LoadingProvider.jsx │ ├── ButtonWithLoadingContextAndKey.jsx │ ├── withLoadingContext.jsx │ ├── HomePageWithProvider.jsx │ ├── withLoadingContextAndKey.jsx │ ├── ButtonWithModal.jsx │ ├── CreateMemberModalContent.jsx │ ├── LoadingProviderWithKey.jsx │ ├── Modal.jsx │ ├── ButtonWithNewConsumer.jsx │ ├── HomePageWithTwoProvider.jsx │ ├── createModalProvider.jsx │ ├── ButtonWithConsumer.jsx │ ├── ModalProvider.jsx │ └── HomePageComponent.jsx ├── doit-ui │ ├── Modal │ │ ├── context.jsx │ │ ├── index.jsx │ │ └── create.jsx │ ├── app.css │ ├── withStyles.js │ ├── TableHead.jsx │ ├── Option.jsx │ ├── TableRow.jsx │ ├── TableBody.jsx │ ├── Table.jsx │ ├── Card.jsx │ ├── Spacing.jsx │ ├── Toast.jsx │ ├── TableCell.jsx │ └── Theme.js ├── 08 │ ├── constants │ │ └── modals.js │ ├── actions │ │ ├── routerActions.js │ │ ├── searchFilterActions.js │ │ ├── userActions.js │ │ ├── notificationActions.js │ │ ├── transactionApiActions.js │ │ └── transactionActions.js │ ├── Api.js │ ├── selectors │ │ └── userSelectors.js │ ├── containers │ │ ├── NotificationContainer.jsx │ │ ├── main │ │ │ ├── NotificationContainer.jsx │ │ │ ├── TradeCoinPageContainer.jsx │ │ │ ├── TransactionSearchFilterContainer.jsx │ │ │ ├── TransactionPaginationContainer.jsx │ │ │ └── TransactionListContainer.jsx │ │ ├── signup │ │ │ └── RegisterPageContainer.jsx │ │ └── RouterStateContainer.jsx │ ├── reducers │ │ ├── routerReducer.js │ │ ├── index.js │ │ ├── searchFilterReducer.js │ │ └── notificationReducer.js │ ├── ModalProvider.jsx │ ├── components │ │ ├── main │ │ │ ├── MainPage.jsx │ │ │ ├── Notification.jsx │ │ │ ├── CoinOverview.jsx │ │ │ ├── TransactionPagination.jsx │ │ │ └── CoinDashlet.jsx │ │ ├── Notification.jsx │ │ ├── NotFound.jsx │ │ ├── AppLayout.jsx │ │ └── AppNav.jsx │ ├── middlewares │ │ ├── transactionEffects.js │ │ ├── searchFilterEffects.js │ │ ├── routerEffects.js │ │ └── notificationEffects.js │ └── CoinApp.jsx ├── 07 │ ├── actions │ │ ├── userActions.js │ │ ├── collectionActions02.js │ │ ├── collectionActions01.js │ │ ├── loadingActions.js │ │ ├── collectionActions.js │ │ └── searchFilterActions.js │ ├── reducers │ │ ├── collectionReducer01.js │ │ ├── userReducer.js │ │ ├── index.js │ │ ├── loadingReducer.js │ │ ├── searchFilterReducer.js │ │ ├── collectionReducer02.js │ │ └── collectionReducer.js │ ├── configureStore.js │ ├── containers │ │ ├── DispatchContainer02.jsx │ │ ├── ContainerComponent.jsx │ │ ├── DispatchContainer01.jsx │ │ ├── SearchFilterInputContainer.jsx │ │ ├── DispatchContainer03.jsx │ │ ├── SearchResetButtonContainer.jsx │ │ └── SearchResultTableContainer.jsx │ ├── ReduxApp01.jsx │ ├── ActionComponent01.jsx │ ├── ReduxApp02.jsx │ ├── AdvReduxApp03.jsx │ ├── PresentationComponent.jsx │ ├── AdvReduxApp02.jsx │ ├── AdvReduxApp01.jsx │ ├── AdvReduxApp08.jsx │ ├── AdvReduxApp04.jsx │ ├── SearchResultTable.jsx │ ├── AdvReduxApp05.jsx │ ├── ActionComponent02.jsx │ ├── AdvReduxApp06.jsx │ ├── ReduxApp03.jsx │ ├── AdvReduxApp07.jsx │ └── SearchFilterReduxApp.jsx ├── 01 │ ├── RSC.jsx │ ├── npm_yarn │ ├── RCC.jsx │ ├── RSCP.jsx │ ├── RCCP.jsx │ ├── RPC.jsx │ └── RCFC.jsx ├── sass │ ├── components │ │ ├── _icons-material-design.scss │ │ ├── _transitions.scss │ │ ├── forms │ │ │ ├── _forms.scss │ │ │ └── _file-input.scss │ │ ├── _pulse.scss │ │ ├── _table_of_contents.scss │ │ ├── _tooltip.scss │ │ ├── _color-classes.scss │ │ ├── _materialbox.scss │ │ ├── _toast.scss │ │ ├── _badges.scss │ │ └── _modal.scss │ └── materialize.scss ├── stories │ ├── NewCounterStory.jsx │ ├── BooleanComponentStory.jsx │ ├── DoitUIToastStory.jsx │ ├── BranchStory.jsx │ ├── WithLifecyleStory.jsx │ ├── WithStateStory.jsx │ ├── LifecycleStory.jsx │ ├── WithErrorStory.jsx │ ├── ContextStory.jsx │ ├── InputWithStyleStory.jsx │ ├── TextStory.jsx │ ├── WithHoCStory.jsx │ ├── InputStory.jsx │ ├── DoitUIInputStory.jsx │ ├── DoitUICardStory.jsx │ ├── WithLoadingStory.jsx │ ├── FormStory.jsx │ ├── DoitUISpacingStory.jsx │ ├── DoitUISelectStory.jsx │ ├── ChecBoxStory.jsx │ ├── DoitUITableStory.jsx │ ├── DoitUITextStory.jsx │ ├── DoitUIInlineListStory.jsx │ ├── ReduxStory.jsx │ ├── DoitUIButtonStory.jsx │ └── ButtonStory.jsx ├── 03 │ ├── TodaysPlan.jsx │ ├── PureComponent.jsx │ ├── ChildProperty.jsx │ ├── StateExampleApp.jsx │ ├── LifecycleExampleApp.jsx │ ├── PropsParentComponent.jsx │ ├── DefaultPropsExample.jsx │ ├── JSXSample.jsx │ ├── SFCTodaysPlan.jsx │ ├── ListExample.jsx │ ├── PropsComponent.jsx │ ├── ChildComponentExample.jsx │ ├── TodaysPlanApp.jsx │ ├── BooleanPropExample.jsx │ ├── non-jsx-sample.js │ ├── TodoList.jsx │ ├── BooleanComponent.jsx │ ├── SFC.jsx │ ├── Counter2.jsx │ ├── ParentComponent.jsx │ ├── UnmountLifecycleExampleApp.jsx │ ├── Counter.jsx │ ├── CounterApp.jsx │ ├── ParentComponentWithConst.jsx │ ├── ChildComponent2.jsx │ ├── CounterExample.jsx │ ├── OrderForm.jsx │ ├── Counter3.jsx │ ├── DefaultPropsCompoent.jsx │ ├── ForceUpdateExample.jsx │ ├── ScrollSpy.jsx │ ├── NewCounter.jsx │ ├── ChildComponent.jsx │ ├── LifecycleExample.jsx │ ├── StateExample.jsx │ └── Input.jsx ├── App.test.js ├── index.css ├── 05 │ ├── withHoC.jsx │ ├── branch.jsx │ ├── withLoading.jsx │ ├── compose.jsx │ ├── lifecycle.jsx │ ├── 05-1-currying.js │ ├── withState.jsx │ └── withError.jsx ├── __tests__ │ ├── 04 │ │ ├── Text.test.jsx │ │ └── CheckBox.test.jsx │ ├── 03 │ │ └── Input.test.jsx │ └── 05 │ │ ├── withError.test.jsx │ │ └── withLoading.test.jsx ├── 04 │ ├── withStyles.jsx │ ├── Theme.js │ ├── Text.jsx │ └── CheckBox.jsx ├── 09 │ └── thunkActions.js ├── App.jsx ├── setupTests.js ├── App.css └── index.js ├── do-it-react-cover.jpeg ├── .storybook ├── addons.js ├── webpack.config.js └── config.js ├── .prettierrc ├── static └── app.css ├── pages ├── index.jsx ├── register.jsx ├── _document.js └── _app.js ├── mock ├── fake.js ├── db.json └── create.js ├── .gitignore └── firebase.json /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | distDir: "./functions/next" 3 | }; 4 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "justin-do-it-react" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinpark/justin-do-it-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Roboto, Tahoma, Verdana; 3 | font-size: 16px; 4 | } 5 | -------------------------------------------------------------------------------- /do-it-react-cover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinpark/justin-do-it-react/HEAD/do-it-react-cover.jpeg -------------------------------------------------------------------------------- /src/02/MyModule.js: -------------------------------------------------------------------------------- 1 | export const ModuleName = 'It is my module'; 2 | export default function MyModule() {}; 3 | -------------------------------------------------------------------------------- /src/06/FormContext.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const { Provider, Consumer } = React.createContext({}); 4 | -------------------------------------------------------------------------------- /src/06/ModalContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const { Provider, Consumer } = React.createContext({}); 4 | -------------------------------------------------------------------------------- /src/doit-ui/Modal/context.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export const { Provider, Consumer } = React.createContext({}); 3 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | // 확장 도구는 이곳에 추가합니다. 3 | import 'storybook-addon-jsx/register'; 4 | -------------------------------------------------------------------------------- /src/08/constants/modals.js: -------------------------------------------------------------------------------- 1 | export const TRADE_COIN_MODAL = 'modal/TRADE_COIN_MODAL'; 2 | export const REGISTER_USER_MODAL = 'modal/REGISTER_USER_MODAL'; 3 | -------------------------------------------------------------------------------- /src/07/actions/userActions.js: -------------------------------------------------------------------------------- 1 | export const SET_USER = 'user/SET_USER'; 2 | 3 | export const setUser = user => ({ 4 | type: SET_USER, 5 | payload: user, 6 | }); 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 100, 4 | "tabWidth": 2, 5 | "trailingComma": "all", 6 | "semi": true, 7 | "singleQuote": true 8 | } 9 | -------------------------------------------------------------------------------- /src/01/RSC.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const RSC = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default RSC; 12 | -------------------------------------------------------------------------------- /src/01/npm_yarn: -------------------------------------------------------------------------------- 1 | a@^1.0.0: 2 | dependencies: 3 | s "^1.0.0" 4 | s@^1.0.0: 5 | 6 | b@^1.0.0: 7 | dependencies: 8 | s "^2.0.0" 9 | 10 | s@^2.0.0: 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/07/actions/collectionActions02.js: -------------------------------------------------------------------------------- 1 | export const SET_AGE = 'collection/SET_AGE'; 2 | 3 | export const setAge = (id, age) => ({ 4 | type: SET_AGE, 5 | payload: { id, age }, 6 | }); 7 | -------------------------------------------------------------------------------- /src/07/actions/collectionActions01.js: -------------------------------------------------------------------------------- 1 | export const SET_COLLECTION = 'collection/SET_COLLECTION'; 2 | 3 | export const setCollection = collection => ({ 4 | type: SET_COLLECTION, 5 | payload: collection, 6 | }); 7 | -------------------------------------------------------------------------------- /src/08/actions/routerActions.js: -------------------------------------------------------------------------------- 1 | export const SET_LOCATION = 'router/SET_LOCATION'; 2 | 3 | export function setLocation(location) { 4 | return { 5 | type: SET_LOCATION, 6 | payload: { location }, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/13/AsyncMainPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AsyncComponent from './AsyncComponent'; 3 | 4 | export default () => ( 5 | import('../08/components/main/MainPage')} /> 6 | ); 7 | -------------------------------------------------------------------------------- /src/08/Api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const isDev = process.env.NODE_ENV === 'development'; 4 | const Api = axios.create({ 5 | baseURL: isDev ? 'http://localhost:4000/' : '/api/', 6 | }); 7 | 8 | export default Api; 9 | -------------------------------------------------------------------------------- /src/08/selectors/userSelectors.js: -------------------------------------------------------------------------------- 1 | import createSelectors from '../../11/api-redux-pack/createSelectors'; 2 | 3 | export const { createLoadingStateSelector: userCreateLoadingStateSelector } = createSelectors( 4 | 'users', 5 | ); 6 | -------------------------------------------------------------------------------- /src/01/RCC.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class RCC extends Component { 4 | render() { 5 | var text = '따옴표'; 6 | return
{text}
; 7 | } 8 | } 9 | 10 | export default RCC; 11 | -------------------------------------------------------------------------------- /src/13/AsyncRegisterPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AsyncComponent from './AsyncComponent'; 3 | 4 | export default () => ( 5 | import('../08/containers/signup/RegisterPageContainer')} /> 6 | ); 7 | -------------------------------------------------------------------------------- /src/sass/components/_icons-material-design.scss: -------------------------------------------------------------------------------- 1 | /* This is needed for some mobile phones to display the Google Icon font properly */ 2 | .material-icons { 3 | text-rendering: optimizeLegibility; 4 | font-feature-settings: 'liga'; 5 | } 6 | -------------------------------------------------------------------------------- /static/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: "Roboto", "Helvetica", "Arial", sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | background-color: #eeeeee; 8 | } 9 | -------------------------------------------------------------------------------- /src/doit-ui/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: "Roboto", "Helvetica", "Arial", sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | background-color: #eeeeee; 8 | } 9 | -------------------------------------------------------------------------------- /src/stories/NewCounterStory.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import NewCounter from '../03/NewCounter'; 5 | 6 | storiesOf('NewCounter', module).add('기본 설정', () => ); 7 | -------------------------------------------------------------------------------- /src/01/RSCP.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const RSCP = props => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | 12 | RSCP.propTypes = { 13 | 14 | }; 15 | 16 | export default RSCP; 17 | -------------------------------------------------------------------------------- /pages/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import MainPage from '../src/08/components/main/MainPage'; 3 | 4 | class IndexDocument extends PureComponent { 5 | render() { 6 | return ; 7 | } 8 | } 9 | 10 | export default IndexDocument; 11 | -------------------------------------------------------------------------------- /src/03/TodaysPlan.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class TodaysPlan extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 놀러가자 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default TodaysPlan; 14 | -------------------------------------------------------------------------------- /src/07/reducers/collectionReducer01.js: -------------------------------------------------------------------------------- 1 | const initState = { 2 | ids: [], 3 | entities: {}, 4 | }; 5 | 6 | export default (state = initState, action) => { 7 | const { type, payload } = action; 8 | 9 | switch (type) { 10 | default: 11 | return state; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | module: { 5 | rules: [ 6 | { 7 | test: /\.scss$/, 8 | loaders: ["sass-loader"], 9 | include: path.resolve(__dirname, "../") 10 | } 11 | ] 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/01/RCCP.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class RCCP extends Component { 5 | render() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | } 13 | 14 | RCCP.propTypes = { 15 | 16 | }; 17 | 18 | export default RCCP; 19 | -------------------------------------------------------------------------------- /src/07/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | import reducers from './reducers'; 4 | 5 | export default initStates => createStore( 6 | combineReducers(reducers), 7 | initStates, 8 | composeWithDevTools(), 9 | ); 10 | -------------------------------------------------------------------------------- /src/stories/BooleanComponentStory.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import BooleanComponent from '../03/BooleanComponent'; 5 | 6 | storiesOf('BooleanComponent', module) 7 | .add('기본 설정', () => ) 8 | .add('bored 설정', () => ); 9 | -------------------------------------------------------------------------------- /src/01/RPC.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class RPC extends PureComponent { 5 | render() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | } 13 | 14 | RPC.propTypes = { 15 | 16 | }; 17 | 18 | export default RPC; 19 | -------------------------------------------------------------------------------- /src/07/actions/loadingActions.js: -------------------------------------------------------------------------------- 1 | export const SET_LOADING = 'loading/SET_LOADING'; 2 | export const RESET_LOADING = 'loading/RESET_LOADING'; 3 | 4 | export const setLoading = loading => ({ 5 | type: SET_LOADING, 6 | payload: loading, 7 | }); 8 | 9 | export const resetLoading = () => ({ 10 | type: RESET_LOADING 11 | }); 12 | -------------------------------------------------------------------------------- /src/07/containers/DispatchContainer02.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ActionComponent from '../ActionComponent'; 3 | import { setAge } from '../actions/collectionActions'; 4 | 5 | const mapDispatchToProps = { 6 | setAge, 7 | }; 8 | 9 | export default connect(null, mapDispatchToProps)(ActionComponent); 10 | -------------------------------------------------------------------------------- /src/03/PureComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import shallowEqual from 'shallow-equal'; 3 | 4 | export class PureComponent extends React.Component { 5 | shouldComponentUpdate(nextProps, nextState) { 6 | return !shallowEqual(this.props, nextProps) || 7 | !shallowEqual(this.state, nextState) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/11/api-redux-pack/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const FETCH_LIST = 'api-redux-pack/FETCH_LIST'; 2 | export const FETCH = 'api-redux-pack/FETCH'; 3 | export const DELETE = 'api-redux-pack/DELETE'; 4 | export const UPDATE = 'api-redux-pack/UPDATE'; 5 | export const CREATE = 'api-redux-pack/CREATE'; 6 | export const RESET = 'api-redux-pack/RESET'; 7 | -------------------------------------------------------------------------------- /mock/fake.js: -------------------------------------------------------------------------------- 1 | const jsonServer = require('json-server'); 2 | const server = jsonServer.create(); 3 | const middlewares = jsonServer.defaults(); 4 | const port = 4000; 5 | 6 | server.use(middlewares); 7 | server.use((req, res) => { 8 | res.status(500).jsonp({ 9 | errorMessage: '문제가 발생했습니다', 10 | }); 11 | }); 12 | server.listen(port); 13 | -------------------------------------------------------------------------------- /src/03/ChildProperty.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class ChildProperty extends Component { 5 | render() { 6 | return
{this.props.children}
; 7 | } 8 | } 9 | ChildProperty.propTypes = { 10 | children: PropTypes.node, 11 | }; 12 | export default ChildProperty; 13 | -------------------------------------------------------------------------------- /src/03/StateExampleApp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StateExample from './03/StateExample'; 3 | 4 | class App extends React.Component { 5 | render() { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/sass/components/_transitions.scss: -------------------------------------------------------------------------------- 1 | // Scale transition 2 | .scale-transition { 3 | &.scale-out { 4 | transform: scale(0); 5 | transition: transform .2s !important; 6 | } 7 | 8 | &.scale-in { 9 | transform: scale(1); 10 | } 11 | 12 | transition: transform .3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important; 13 | } -------------------------------------------------------------------------------- /src/02/quiz: -------------------------------------------------------------------------------- 1 | 2 | function getTotal(cart) { 3 | return cart.amount * cart.price; 4 | } 5 | var cart = { 6 | name: '도서', 7 | amount: 5, 8 | price: 300, 9 | }; 10 | var product = {}; 11 | var myCart = '장바구니에 ' + cart.name + '가 있습니다. 총 금액은 ' + getTotal(cart) + '입니다.'; 12 | 13 | var myCart = `장바구니에 ${cart.name}가 있습니다. 총 금액은 ${getTotal(cart)}입니다.`; 14 | --- 15 | -------------------------------------------------------------------------------- /src/03/LifecycleExampleApp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LifecycleExample from './03/LifecycleExample'; 3 | 4 | class App extends React.Component { 5 | render() { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/07/actions/collectionActions.js: -------------------------------------------------------------------------------- 1 | export const SET_COLLECTION = 'collection/SET_COLLECTION'; 2 | export const SET_AGE = 'collection/SET_AGE'; 3 | 4 | export const setCollection = collection => ({ 5 | type: SET_COLLECTION, 6 | payload: collection, 7 | }); 8 | 9 | export const setAge = (id, age) => ({ 10 | type: SET_AGE, 11 | payload: { id, age }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/03/PropsParentComponent.jsx: -------------------------------------------------------------------------------- 1 | // src폴더안에 App.js을 연 다음 기존 내용을 모두 지우고 아래의 내용으로 작성해 보세요 2 | import React from 'react'; 3 | import PropComponent from './03/PropComponent'; 4 | 5 | class App extends React.Component { 6 | render() { 7 | return ( 8 | 11 | ); 12 | } 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /src/03/DefaultPropsExample.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DefaultPropsComponent from './03/DefaultPropsComponent'; 3 | 4 | class App extends React.Component { 5 | render() { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/08/containers/NotificationContainer.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Notification from '../components/Notification'; 3 | 4 | const mapStateToProps = state => { 5 | const { showMessage, message, warning } = state.notification; 6 | 7 | return { showMessage, message, warning }; 8 | }; 9 | 10 | export default connect(mapStateToProps)(Notification); 11 | -------------------------------------------------------------------------------- /src/08/containers/main/NotificationContainer.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Notification from '../../components/main/Notification'; 3 | 4 | const mapStateToProps = state => { 5 | const { hasError, errorMessage } = state.transactions; 6 | 7 | return { hasError, errorMessage }; 8 | }; 9 | 10 | export default connect(mapStateToProps)(Notification); 11 | -------------------------------------------------------------------------------- /src/08/containers/main/TradeCoinPageContainer.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import TradeCoinPage from '../../components/main/TradeCoinPage'; 3 | // import { createTransaction } from '../../actions/transactionActions'; 4 | import { createTransaction } from '../../actions/transactionPackActions'; 5 | 6 | export default connect(null, { createTransaction })(TradeCoinPage); 7 | -------------------------------------------------------------------------------- /src/08/actions/searchFilterActions.js: -------------------------------------------------------------------------------- 1 | export const SET_FILTER = 'searchFilter/SET_FILTER'; 2 | export const RESET_FILTER = 'searchFilter/RESET_FILTER'; 3 | 4 | export function setFilter(params) { 5 | return { 6 | type: SET_FILTER, 7 | payload: { params }, 8 | } 9 | } 10 | 11 | export function resetFilter() { 12 | return { 13 | type: RESET_FILTER, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/07/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_USER } from '../actions/userActions'; 2 | 3 | export default function reducer(state = {}, action) { 4 | const { type, payload } = action; 5 | 6 | switch(type) { 7 | case SET_USER: { 8 | return { 9 | ...state, 10 | ...payload, 11 | }; 12 | } 13 | default: 14 | return state; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/08/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import createActions from '../../11/api-redux-pack/createActions'; 2 | 3 | const { create } = createActions('users'); 4 | 5 | export function createUser(data, onComplete) { 6 | return create( 7 | data, 8 | {}, 9 | { 10 | notification: { success: '회원 가입이 성공적으로 완료되었습니다.' }, 11 | onSuccess: onComplete, 12 | }, 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/07/containers/ContainerComponent.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import PresentationComponent from '../PresentationComponent'; 3 | 4 | const mapStateToProps = (state, props) => { 5 | return { 6 | userName: state.user.name, 7 | entity: state.collection.entities[props.id], 8 | }; 9 | }; 10 | 11 | export default connect(mapStateToProps)(PresentationComponent); 12 | -------------------------------------------------------------------------------- /src/stories/DoitUIToastStory.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import Toast from '../doit-ui/Toast'; 5 | 6 | storiesOf('Doit-UI/Toast', module) 7 | .addWithJSX('Toast 예제', () => ( 8 | 9 | )) 10 | .addWithJSX('Toast warning 예제', () => ( 11 | 12 | )); 13 | -------------------------------------------------------------------------------- /src/stories/BranchStory.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import BranchLoadingButton from '../05/branch'; 5 | 6 | storiesOf('Branch', module) 7 | .addWithJSX('기본 설정', () => 안녕하세요) 8 | .addWithJSX('isLoading 예제', () => ( 9 | 안녕하세요 10 | )); 11 | -------------------------------------------------------------------------------- /src/03/JSXSample.jsx: -------------------------------------------------------------------------------- 1 | // src폴더안에 App.js을 연 다음 기존 내용을 모두 지우고 아래의 내용으로 작성해 보세요 2 | import React from 'react'; 3 | 4 | class App extends React.Component { 5 | render() { 6 | return ( 7 | // 아래의 내용이 JSX 양식 입니다. 8 |
9 | 10 |
안녕하세요
11 |
12 | ); 13 | } 14 | } 15 | export default App; 16 | -------------------------------------------------------------------------------- /src/03/SFCTodaysPlan.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TodaysPlan from './03/TodaysPlan'; 3 | 4 | export default function SFCTodaysPlanApp(props) { 5 | const { onButtonClick, hasPlan } = props; 6 | return ( 7 |
8 | {hasPlan ? : null} 9 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/07/containers/DispatchContainer01.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ActionComponent from '../ActionComponent01'; 3 | 4 | import { setAge } from '../actions/collectionActions'; 5 | 6 | const mapDispatchToProps = dispatch => { 7 | return { 8 | setAge: (id, age) => dispatch(setAge(id, age)), 9 | }; 10 | }; 11 | 12 | export default connect(null, mapDispatchToProps)(ActionComponent); 13 | -------------------------------------------------------------------------------- /src/07/reducers/index.js: -------------------------------------------------------------------------------- 1 | import loading from './loadingReducer'; 2 | import user from './userReducer'; 3 | // import collection from './collectionReducer01'; 4 | // import collection from './collectionReducer02'; 5 | import collection from './collectionReducer'; 6 | import searchFilter from './searchFilterReducer'; 7 | 8 | export default { 9 | collection, 10 | loading, 11 | user, 12 | searchFilter, 13 | }; 14 | -------------------------------------------------------------------------------- /src/03/ListExample.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class ListExample extends React.PureComponent { 4 | render() { 5 | const priceList = [1000, 2000, 3000, 4000]; 6 | const prices = priceList.map((price) =>
가격: {price}원
); 7 | return ( 8 |
9 | 10 | {prices} 11 |
12 | ); 13 | } 14 | } 15 | 16 | export default ListExample; 17 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 5 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 12 | } 13 | -------------------------------------------------------------------------------- /src/02/debounce.js: -------------------------------------------------------------------------------- 1 | export function debounce(func, delay) { 2 | let inDebounce; 3 | return function(...args) { 4 | if (inDebounce) { 5 | clearTimeout(inDebounce); 6 | } 7 | inDebounce = setTimeout( 8 | () => func(...args), 9 | delay); 10 | } 11 | } 12 | 13 | // const run = debounce(val => console.log(val), 100); 14 | 15 | // run('a'); 16 | // run('b'); 17 | // run('2'); 18 | // .... 100ms이후 19 | // 2 20 | -------------------------------------------------------------------------------- /src/03/PropsComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class PropsComponent extends React.Component { 5 | render() { 6 | return ( 7 |
8 | {this.props.name} 9 |
10 | ); 11 | } 12 | } 13 | 14 | // 자료형을 선언하는 예제 15 | PropsComponent.propTypes = { 16 | name: PropTypes.string, 17 | }; 18 | 19 | export default PropsComponent; 20 | -------------------------------------------------------------------------------- /src/stories/WithLifecyleStory.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import { withLoadData } from "../05/lifecycle"; 5 | import Counter from "../03/Counter"; 6 | 7 | const CounterWithLoadData = withLoadData(Counter); 8 | 9 | storiesOf("WithLifecycle", module) 10 | .addWithJSX("CounterWithLoadData", () => ( 11 | Promise.resolve(10)} /> 12 | )); 13 | -------------------------------------------------------------------------------- /src/03/ChildComponentExample.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ChildComponent2 from './ChildComponent2'; 3 | 4 | class App extends React.Component { 5 | render() { 6 | return ( 7 |
8 |
9 | 13 |
14 |
15 | ); 16 | } 17 | } 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /src/08/actions/notificationActions.js: -------------------------------------------------------------------------------- 1 | export const SHOW_NOTIFICATION = 'notification/SHOW_NOTIFICATION'; 2 | export const HIDE_NOTIFICATION = 'notification/HIDE_NOTIFICATION'; 3 | 4 | export function showMessage(message, warning = false) { 5 | return { 6 | type: SHOW_NOTIFICATION, 7 | payload: { message, warning }, 8 | } 9 | } 10 | 11 | export function hideMessage() { 12 | return { 13 | type: HIDE_NOTIFICATION, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/08/reducers/routerReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_LOCATION } from '../actions/routerActions'; 2 | 3 | const initState = { 4 | location: {}, 5 | }; 6 | 7 | export default (state = initState, action) => { 8 | const { type, payload } = action; 9 | 10 | switch (type) { 11 | case SET_LOCATION: { 12 | const { location } = payload; 13 | return { ...state, location }; 14 | } 15 | default: 16 | return state; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .firebase 25 | .next 26 | next 27 | -------------------------------------------------------------------------------- /src/05/withHoC.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function withHoC(WrappedComponent) { 4 | const { displayName, name: componentName } = WrappedComponent; 5 | const wrappedComponentName = displayName || componentName; 6 | return class WithHoC extends React.Component { 7 | static displayName = `withHoC(${wrappedComponentName})`; 8 | render() { 9 | return ; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/03/TodaysPlanApp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TodaysPlan from './03/TodaysPlan'; 3 | 4 | export default class TodaysPlanApp extends React.Component { 5 | render() { 6 | const { onButtonClick, hasPlan } = this.props; 7 | return ( 8 |
9 | {hasPlan ? : null} 10 | 13 |
14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/08/containers/signup/RegisterPageContainer.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import RegisterPage from '../../components/signup/RegisterPage'; 3 | import { createUser } from '../../actions/userActions'; 4 | import { userCreateLoadingStateSelector } from '../../selectors/userSelectors'; 5 | 6 | export default connect( 7 | state => ({ 8 | loading: userCreateLoadingStateSelector(state), 9 | }), 10 | { createUser }, 11 | )(RegisterPage); 12 | -------------------------------------------------------------------------------- /src/__tests__/04/Text.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Text from '../../04/Text'; 5 | 6 | describe('', () => { 7 | it('renders without crashing', () => { 8 | expect(() => { 9 | shallow(테스트); 10 | }).not.toThrow(); 11 | }); 12 | 13 | it('contains ', () => { 14 | expect(shallow(테스트).dive().find('span')).toHaveLength(1); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/03/BooleanPropExample.jsx: -------------------------------------------------------------------------------- 1 | // src폴더안에 App.js을 연 다음 기존 내용을 모두 지우고 아래의 내용으로 작성해 보세요 2 | import React from 'react'; 3 | import BooleanComponent from './03/BooleanComponent'; 4 | 5 | class App extends React.Component { 6 | render() { 7 | return ( 8 |
9 |
지루할 때:
10 |
즐거울 때:
11 |
12 | ); 13 | } 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/07/reducers/loadingReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_LOADING, RESET_LOADING } from '../actions/loadingActions'; 2 | 3 | const initState = false; 4 | 5 | export default function reducer (state = initState, action) { 6 | const { type, payload } = action; 7 | switch(type) { 8 | case SET_LOADING: { 9 | return payload; 10 | } 11 | case RESET_LOADING: { 12 | return initState; 13 | } 14 | default: 15 | return state; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/doit-ui/withStyles.js: -------------------------------------------------------------------------------- 1 | import ThemedStyleSheet from 'react-with-styles/lib/ThemedStyleSheet'; 2 | import aphroditeInterface from 'react-with-styles-interface-aphrodite'; 3 | import { css, withStyles, withStylesPropTypes } from 'react-with-styles'; 4 | import Theme from './Theme'; 5 | 6 | ThemedStyleSheet.registerInterface(aphroditeInterface); 7 | ThemedStyleSheet.registerTheme(Theme); 8 | 9 | export { css, withStyles, withStylesPropTypes, ThemedStyleSheet }; 10 | -------------------------------------------------------------------------------- /src/__tests__/03/Input.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Input from '../../03/Input'; 4 | 5 | describe('', () => { 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render(, div); 9 | ReactDOM.unmountComponentAtNode(div); 10 | expect(React.isValidElement()).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/03/non-jsx-sample.js: -------------------------------------------------------------------------------- 1 | // src폴더안에 index.js을 연 다음 기존 내용을 모두 지우고 아래의 내용으로 작성해 보세요 2 | var img = document.createElement('img'); 3 | img.setAttribute('src', 'http://www.easyspub.co.kr/images/logo_footer.png'); 4 | var divEl = document.createElement('div'); 5 | divEl.innerText = '안녕하세요'; 6 | var welcomeEl = document.createElement('div'); 7 | welcomeEl.append(img); 8 | welcomeEl.append(divEl); 9 | 10 | var root = document.getElementById('root'); 11 | root.append(welcomeEl); 12 | -------------------------------------------------------------------------------- /src/03/TodoList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class TodoList extends React.PureComponent { 4 | render() { 5 | const todoList = [ 6 | { taskName: '빨래하기', finished: false }, 7 | { taskName: '공부하기', finished: true }, 8 | ]; 9 | return ( 10 |
11 | {todoList.map(todo => ( 12 |
{todo.taskName}
13 | ))} 14 |
15 | ); 16 | } 17 | } 18 | 19 | export default TodoList; 20 | -------------------------------------------------------------------------------- /src/03/BooleanComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class BooleanComponent extends React.Component { 5 | render() { 6 | // 불리언 타입을 조건문에 적용한 예제 7 | const message = this.props.bored ? '놀러 가자' : '하던 일 열심히 마무리하기'; 8 | return
{message}
; 9 | } 10 | } 11 | 12 | // 자료형을 선언하는 예제 13 | BooleanComponent.propTypes = { 14 | bored: PropTypes.bool, 15 | }; 16 | 17 | export default BooleanComponent; 18 | -------------------------------------------------------------------------------- /src/04/withStyles.jsx: -------------------------------------------------------------------------------- 1 | import ThemedStyleSheet from 'react-with-styles/lib/ThemedStyleSheet'; 2 | import aphroditeInterface from 'react-with-styles-interface-aphrodite'; 3 | import { css, withStyles, withStylesPropTypes } from 'react-with-styles'; 4 | import Theme from './Theme'; 5 | 6 | ThemedStyleSheet.registerTheme(Theme); 7 | ThemedStyleSheet.registerInterface(aphroditeInterface); 8 | 9 | export { css, withStyles, withStylesPropTypes, ThemedStyleSheet }; 10 | export default withStyles; 11 | -------------------------------------------------------------------------------- /src/stories/WithStateStory.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import { CounterWithCountState, CounterWithCountHandler } from '../05/withState'; 5 | import Counter from '../03/Counter'; 6 | 7 | storiesOf('WithState', module) 8 | .addWithJSX('CounterWithCountState', () => ) 9 | .addWithJSX('CounterWithCountHandler', () => ) 10 | .addWithJSX('CounterWithoutHoC', () => ); 11 | -------------------------------------------------------------------------------- /src/stories/LifecycleStory.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import { PageWithLoadData, PageWithLoadDataAndLoading } from '../05/lifecycle'; 5 | 6 | storiesOf('Lifecycle', module) 7 | .addWithJSX('loadData 예제', () => ( 8 | fetch('/').then(() => 'hello')} /> 9 | )) 10 | .addWithJSX('로딩 메시지 예제', () => ( 11 | fetch('/').then(() => 'hello')} /> 12 | )); 13 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const next = require('next'); 3 | const apiserver = require('./apiserver'); 4 | 5 | var dev = process.env.NODE_ENV !== 'production'; 6 | var app = next({ dev, conf: { distDir: 'next' } }); 7 | var handle = app.getRequestHandler(); 8 | 9 | exports.next = functions.https.onRequest((req, res) => { 10 | return app.prepare().then(() => handle(req, res)); 11 | }); 12 | 13 | exports.apiserver = functions.https.onRequest(apiserver); 14 | -------------------------------------------------------------------------------- /src/08/reducers/index.js: -------------------------------------------------------------------------------- 1 | // import transactions from './transactionsReducer'; 2 | import notification from './notificationReducer'; 3 | import createReducers from '../../11/api-redux-pack/createReducers'; 4 | import searchFilter from './searchFilterReducer'; 5 | import router from './routerReducer'; 6 | 7 | const apiReducers = createReducers('transactions', 'users'); 8 | 9 | export default { 10 | ...apiReducers, 11 | notification, 12 | searchFilter, 13 | router, 14 | // transactions, 15 | }; 16 | -------------------------------------------------------------------------------- /src/08/ModalProvider.jsx: -------------------------------------------------------------------------------- 1 | import createProvider from '../doit-ui/Modal/create'; 2 | import { TRADE_COIN_MODAL, REGISTER_USER_MODAL } from './constants/modals'; 3 | // import TradeCoinPage from './components/main/TradeCoinPage01'; 4 | import TradeCoinPage from './containers/main/TradeCoinPageContainer'; 5 | import RegisterPage from './containers/signup/RegisterPageContainer'; 6 | 7 | export default createProvider({ 8 | [TRADE_COIN_MODAL]: TradeCoinPage, 9 | [REGISTER_USER_MODAL]: RegisterPage, 10 | }); 11 | -------------------------------------------------------------------------------- /src/06/ButtonWithContext.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Button from '../04/Button'; 4 | 5 | function ButtonWithContext({ children }, context) { 6 | const { loading, setLoading } = context; 7 | 8 | return ; 9 | } 10 | 11 | ButtonWithContext.contextTypes = { 12 | loading: PropTypes.bool, 13 | setLoading: PropTypes.func, 14 | }; 15 | 16 | export default ButtonWithContext; 17 | -------------------------------------------------------------------------------- /src/07/containers/SearchFilterInputContainer.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { setFilter } from '../actions/searchFilterActions'; 3 | import Input from '../../04/InputWithStyle'; 4 | 5 | const mapStateToProps = (state, props) => { 6 | const value = state.searchFilter[props.name] || ''; 7 | 8 | return { 9 | value, 10 | }; 11 | }; 12 | 13 | const mapDispatchToProps = { 14 | onChange: setFilter, 15 | }; 16 | 17 | export default connect(mapStateToProps, mapDispatchToProps)(Input); 18 | -------------------------------------------------------------------------------- /src/05/branch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import branch from 'recompose/branch'; 3 | 4 | import Button from '../04/Button'; 5 | 6 | // export default branch( 7 | // ({ isLoading }) => isLoading, 8 | // () => () => , 9 | // )(Button); 10 | 11 | function isLoading(props) { 12 | return props.isLoading; 13 | } 14 | 15 | function LoadingButton(props) { 16 | return ; 17 | } 18 | 19 | export default branch(isLoading, () => LoadingButton)(Button); 20 | -------------------------------------------------------------------------------- /src/sass/components/forms/_forms.scss: -------------------------------------------------------------------------------- 1 | // Remove Focus Boxes 2 | select:focus { 3 | outline: $select-focus; 4 | } 5 | 6 | button:focus { 7 | outline: none; 8 | background-color: $button-background-focus; 9 | } 10 | 11 | label { 12 | font-size: $label-font-size; 13 | color: $input-border-color; 14 | } 15 | 16 | @import 'input-fields'; 17 | @import 'radio-buttons'; 18 | @import 'checkboxes'; 19 | @import 'switches'; 20 | @import 'select'; 21 | @import 'file-input'; 22 | @import 'range'; 23 | -------------------------------------------------------------------------------- /src/03/SFC.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | function SFC(props, context) { 5 | // 클래스 컴포넌트의 this.props값과 동일합니다. 6 | const { somePropValue } = props; 7 | // 클래스 컴포넌트의 this.context와 동일합니다. 8 | // context는 차후에 자세히 다룰 예정입니다. 9 | const { someContextValue } = context; 10 | return

Hello, {somePropValue}

; 11 | } 12 | 13 | SFC.propTypes = { somePropValue: PropTypes.any }; 14 | SFC.defaultProps = { somePropValue: 'default value' }; 15 | 16 | export default SFC; 17 | -------------------------------------------------------------------------------- /mock/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "transactions": [ 3 | { 4 | "id": 1, 5 | "code": "BTX", 6 | "name": "비트코인(BTX)", 7 | "totalPrice": 12391230000, 8 | "currentPrice": 1020000, 9 | "amount": 0.23, 10 | "datetime": "2019/01/20 08:23:22" 11 | }, 12 | { 13 | "id": 2, 14 | "code": "RXP", 15 | "name": "리플(RXP)", 16 | "totalPrice": 1230000, 17 | "currentPrice": 20000, 18 | "amount": 10, 19 | "datetime": "2019/01/20 07:23:22" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/07/ReduxApp01.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 | render() { 13 | return 리덕스 예제; 14 | } 15 | } 16 | 17 | export default ReduxApp; 18 | -------------------------------------------------------------------------------- /src/09/thunkActions.js: -------------------------------------------------------------------------------- 1 | function pureAction(somePayload) { 2 | return { 3 | type: 'SOME_ACTION_TYPE', 4 | payload: somePayload, 5 | }; 6 | } 7 | 8 | function thunkAction(somePayload) { 9 | return function(dispatch, getState) { 10 | dispatch({ 11 | type: 'SOME_ACTION_TYPE', 12 | payload: somePayload, 13 | }); 14 | dispatch({ 15 | type: 'SOME_EXTRA_ACTION_TYPE', 16 | payload: { 17 | ...getState().resource, 18 | somePayload, 19 | } 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/stories/WithErrorStory.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import Input from '../04/InputWithStyle'; 5 | import withError from '../05/withError'; 6 | 7 | const InputWithError = withError('올바르지 못한 값입니다')(Input); 8 | 9 | storiesOf('WithError', module) 10 | .addWithJSX('기본 설정', () => ( 11 | 12 | )) 13 | .addWithJSX('errorMessage 예제', () => ( 14 | 15 | )); 16 | -------------------------------------------------------------------------------- /src/03/Counter2.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Counter2 extends React.Component { 5 | render() { 6 | return ( 7 |
8 | 현재 카운트: {this.props.count} 9 | 14 |
15 | ); 16 | } 17 | } 18 | 19 | Counter2.propTypes = { 20 | count: PropTypes.number, 21 | onAdd: PropTypes.func, 22 | }; 23 | 24 | export default Counter2; 25 | -------------------------------------------------------------------------------- /src/08/components/main/MainPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import CoinOverview from './CoinOverview'; 3 | // import TransactionList from './TransactionList'; 4 | import TransactionListContainer from '../../containers/main/TransactionListContainer'; 5 | 6 | class MainPage extends PureComponent { 7 | render() { 8 | return ( 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | } 16 | 17 | export default MainPage; 18 | -------------------------------------------------------------------------------- /src/08/reducers/searchFilterReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_FILTER, RESET_FILTER } from '../actions/searchFilterActions'; 2 | 3 | const initState = { 4 | params: {}, 5 | }; 6 | 7 | export default (state = initState, action) => { 8 | const { type, payload } = action; 9 | 10 | switch (type) { 11 | case SET_FILTER: { 12 | const { params } = payload; 13 | return { ...state, params }; 14 | } 15 | case RESET_FILTER: { 16 | return { ...initState }; 17 | } 18 | default: 19 | return state; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/doit-ui/TableHead.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class TableHead extends PureComponent { 5 | render() { 6 | const { children } = this.props; 7 | 8 | return ( 9 | 10 | {React.Children.map(children, child => 11 | React.cloneElement(child, { isHeader: true }) 12 | )} 13 | 14 | ); 15 | } 16 | } 17 | 18 | TableHead.propTypes = { 19 | children: PropTypes.node 20 | }; 21 | 22 | export default TableHead; 23 | -------------------------------------------------------------------------------- /src/06/ModalProviderWithKey.jsx: -------------------------------------------------------------------------------- 1 | import createModalProvider from './createModalProvider'; 2 | import DeleteModalContent from './DeleteModalContent'; 3 | import CreateMemberModalContent from './CreateMemberModalContent'; 4 | 5 | export const CONFIRM_DELETE_MODAL = 'confirm_delete_modal'; 6 | export const CREATE_MEMBER_MODAL = 'create_member_modal'; 7 | 8 | const CONTENT_MAP = { 9 | [CONFIRM_DELETE_MODAL]: DeleteModalContent, 10 | [CREATE_MEMBER_MODAL]: CreateMemberModalContent, 11 | }; 12 | 13 | export default createModalProvider(CONTENT_MAP); 14 | -------------------------------------------------------------------------------- /src/07/containers/DispatchContainer03.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ActionComponent from '../ActionComponent02'; 3 | import { setLoading, resetLoading } from '../actions/loadingActions'; 4 | import { setUser } from '../actions/userActions'; 5 | import { setCollection, setAge } from '../actions/collectionActions'; 6 | 7 | const mapDispatchToProps = { 8 | setLoading, 9 | resetLoading, 10 | setUser, 11 | setAge, 12 | setCollection, 13 | }; 14 | 15 | export default connect(null, mapDispatchToProps)(ActionComponent); 16 | -------------------------------------------------------------------------------- /src/08/middlewares/transactionEffects.js: -------------------------------------------------------------------------------- 1 | import { TRADE_COMPLETE, requestTransactionList } from '../actions/transactionActions'; 2 | import { showMessage } from '../actions/notificationActions'; 3 | 4 | export default store => nextRunner => action => { 5 | const { type } = action; 6 | const result = nextRunner(action); 7 | if (type === TRADE_COMPLETE) { 8 | const message = '거래 목록을 최신 정보로 업데이트하였습니다.'; 9 | store.dispatch(showMessage(message)); 10 | store.dispatch(requestTransactionList()); 11 | } 12 | return result; 13 | }; 14 | -------------------------------------------------------------------------------- /src/05/withLoading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (loadingMessage = '로딩중') => WrappedComponent => { 4 | const { displayName, name: componentName } = WrappedComponent; 5 | const wrappedComponentName = displayName || componentName; 6 | 7 | function WithLoading({ isLoading, ...props }) { 8 | if (isLoading) { 9 | return loadingMessage; 10 | } 11 | 12 | return ; 13 | } 14 | WithLoading.displayName = `withLoading(${wrappedComponentName})`; 15 | return WithLoading; 16 | }; 17 | -------------------------------------------------------------------------------- /src/stories/ContextStory.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import HomePageComponent from '../06/HomePageComponent'; 5 | import HomePageWithProvider from '../06/HomePageWithProvider'; 6 | import HomePageWithTwoProvider from '../06/HomePageWithTwoProvider'; 7 | 8 | storiesOf('HomePageComponent', module) 9 | .addWithJSX('컨텍스트 예제', () => ) 10 | .addWithJSX('Provider 예제', () => ) 11 | .addWithJSX('이중 Provider 예제', () => ); 12 | -------------------------------------------------------------------------------- /src/06/NewModalContent.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 NewModalContent() { 7 | return ( 8 | 9 | {({ closeModal }) => ( 10 |
11 |
12 | 13 | 새로운 모달 내용 14 | 15 |
16 | 17 |
18 | )} 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/06/createLoadingConsumer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { DEFAULT_KEY, contextPropTypes } from './LoadingProviderWithKey'; 4 | 5 | export default (contextKey = DEFAULT_KEY) => { 6 | function LoadingConsumer({ render }, context) { 7 | return render(context[contextKey]); 8 | }; 9 | LoadingConsumer.contextTypes = { 10 | [contextKey]: contextPropTypes, 11 | }; 12 | LoadingConsumer.propTypes = { 13 | render: PropTypes.func.isRequired, 14 | }; 15 | return LoadingConsumer; 16 | }; 17 | -------------------------------------------------------------------------------- /src/08/components/main/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 { hasError, errorMessage } = this.props; 8 | return hasError && ( 9 | 10 | ); 11 | } 12 | } 13 | 14 | Notification.propTypes = { 15 | hasError: PropTypes.bool, 16 | errorMessage: PropTypes.string, 17 | }; 18 | 19 | export default Notification; 20 | -------------------------------------------------------------------------------- /src/08/middlewares/searchFilterEffects.js: -------------------------------------------------------------------------------- 1 | import { SET_FILTER } from '../actions/searchFilterActions'; 2 | import { requestTransactionList, resetTransactionList } from '../actions/transactionPackActions'; 3 | 4 | export default store => nextRunner => action => { 5 | const { type, payload } = action; 6 | const result = nextRunner(action); 7 | if (type === SET_FILTER) { 8 | const { params } = payload || {}; 9 | store.dispatch(resetTransactionList()); 10 | store.dispatch(requestTransactionList(params)); 11 | } 12 | return result; 13 | }; 14 | -------------------------------------------------------------------------------- /src/doit-ui/Option.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Option extends PureComponent { 5 | render() { 6 | const { value, label, disabled } = this.props; 7 | 8 | return ( 9 | 12 | ); 13 | } 14 | } 15 | 16 | Option.propTypes = { 17 | disabled: PropTypes.bool, 18 | value: PropTypes.string.isRequired, 19 | label: PropTypes.string, 20 | }; 21 | 22 | export default Option; 23 | -------------------------------------------------------------------------------- /src/03/ParentComponent.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 | return ( 8 | 노드} 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 | 10 | {children} 11 |
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 |
16 |

주문창

17 | 18 | 19 | 20 |
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 | 17 | 18 | 19 |
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 |
18 | 19 | 20 | 21 | 전송 22 | 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 | 17 | 18 | 19 | 20 | ))} 21 |
아이디이름나이
{id}{name}{age}
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 |
14 | 회원가입 15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
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 |