├── containers
├── index.ts
└── Sample.tsx
├── .dockerignore
├── .env.sample
├── static
└── etc
│ ├── js
│ └── hello.js
│ ├── css
│ └── hello.css
│ └── hello.html
├── client
├── api
│ ├── index.ts
│ ├── sampleApi.ts
│ └── service.ts
└── polyfills.js
├── components
├── sample
│ ├── index.ts
│ ├── Sample.tsx
│ ├── __tests__
│ │ └── Login.test.tsx
│ └── Login.tsx
├── SelectLanguage.tsx
└── Layout.tsx
├── actions
├── index.ts
└── sample.ts
├── styles
├── index.ts
├── theme.ts
├── mixin.ts
└── global.ts
├── svgs
├── index.ts
└── Sample.tsx
├── .prettierrc
├── jest.setup.js
├── Dockerfile
├── lang
├── en
│ ├── index.js
│ ├── sample.json
│ └── meta.json
└── ko
│ ├── index.js
│ ├── sample.json
│ └── meta.json
├── nodemon.json
├── server
├── api
│ ├── index.ts
│ ├── sample
│ │ ├── index.ts
│ │ └── controller.ts
│ └── service.ts
└── index.ts
├── tsconfig.test.json
├── tsconfig.server.json
├── sagas
├── index.ts
└── sample.ts
├── reducers
├── index.ts
└── sample.ts
├── lib
└── withIntl.ts
├── ecosystem.config.js
├── jest.config.js
├── constants
└── index.ts
├── .prettierignore
├── .gitignore
├── tsconfig.json
├── .babelrc
├── pages
├── _error.tsx
├── param.tsx
├── index.tsx
├── other.tsx
├── path
│ └── depth.tsx
├── _document.tsx
└── _app.tsx
├── store
└── index.ts
├── next.config.js
├── contexts
└── ThemeProvider.tsx
├── package.json
└── readme.MD
/containers/index.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .next
2 | node_modules
3 | Dockerfile
--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------
1 | URL=
2 | CDN_URL=
3 | PORT=
4 | API_URL=
--------------------------------------------------------------------------------
/static/etc/js/hello.js:
--------------------------------------------------------------------------------
1 | console.log('hello world!!')
--------------------------------------------------------------------------------
/client/api/index.ts:
--------------------------------------------------------------------------------
1 | export {default as sampleApi} from './sampleApi';
--------------------------------------------------------------------------------
/components/sample/index.ts:
--------------------------------------------------------------------------------
1 | export {default as Sample} from './Sample';
--------------------------------------------------------------------------------
/actions/index.ts:
--------------------------------------------------------------------------------
1 | import * as sample from './sample'
2 |
3 | export default {
4 | sample,
5 | }
--------------------------------------------------------------------------------
/styles/index.ts:
--------------------------------------------------------------------------------
1 | import * as theme from './theme';
2 |
3 | export default {
4 | theme,
5 | }
--------------------------------------------------------------------------------
/svgs/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @TODO: svg 사용 방식 예제 작업 필요.
3 | */
4 | export {default as Sample} from './Sample';
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 4,
4 | "semi": false,
5 | "singleQuote": true
6 | }
--------------------------------------------------------------------------------
/static/etc/css/hello.css:
--------------------------------------------------------------------------------
1 | .static-hello {
2 | font-size: 20px;
3 | font-weight: bold;
4 | color: red;
5 | }
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | const Enzyme = require('enzyme')
2 | const Adapter = require('enzyme-adapter-react-16')
3 |
4 | Enzyme.configure({adapter: new Adapter()})
5 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8
2 |
3 | WORKDIR /app
4 |
5 | COPY . .
6 |
7 | RUN yarn
8 | RUN yarn build
9 |
10 | EXPOSE 3000
11 | CMD ["yarn", "prod"]
12 |
--------------------------------------------------------------------------------
/lang/en/index.js:
--------------------------------------------------------------------------------
1 | const sample = require("./sample.json");
2 | const meta = require("./meta.json");
3 |
4 | module.exports = {
5 | ...sample,
6 | ...meta,
7 | };
--------------------------------------------------------------------------------
/lang/ko/index.js:
--------------------------------------------------------------------------------
1 | const sample = require("./sample.json");
2 | const meta = require("./meta.json");
3 |
4 | module.exports = {
5 | ...sample,
6 | ...meta,
7 | };
--------------------------------------------------------------------------------
/lang/ko/sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "sample.helloWorld": "헬로월드",
3 | "sample.boilerplate": "보일러플레이트",
4 | "sample.enterText": "텍스트를 입력하세요.",
5 | "sample": "sample"
6 | }
--------------------------------------------------------------------------------
/lang/en/sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "sample.helloWorld": "HELLO WORLD",
3 | "sample.boilerplate": "Boilerplate",
4 | "sample.enterText": "Please enter text",
5 | "sample": "sample"
6 | }
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server/**/*.ts", "lang/**/*"],
3 | "execMap": {
4 | "ts": "ts-node --typeCheck --compilerOptions \"{\\\"module\\\":\\\"commonjs\\\"}\""
5 | }
6 | }
--------------------------------------------------------------------------------
/client/api/sampleApi.ts:
--------------------------------------------------------------------------------
1 | import service from "./service";
2 |
3 | export default () => {
4 | return service({
5 | method: 'get',
6 | uri: '/sample/api',
7 | })
8 | };
--------------------------------------------------------------------------------
/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import {Router} from 'express';
2 | import sample from './sample';
3 |
4 | const router = Router();
5 |
6 | router.use('/sample', sample);
7 |
8 | export default router;
--------------------------------------------------------------------------------
/server/api/sample/index.ts:
--------------------------------------------------------------------------------
1 | import {Router} from 'express';
2 | import * as controller from './controller';
3 |
4 | const router = Router();
5 |
6 | router.get('/api', controller.api);
7 |
8 | export default router;
--------------------------------------------------------------------------------
/lang/ko/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "meta.title": "헬로월드 - Next.js 보일러플레이트 입니다.",
3 | "meta.description": "쉬운개발을 위한 Next.js 보일러플레이트 입니다.",
4 | "meta.keywords": "next.js, 타입스크립트, 리엑트, styled-components",
5 | "meta": "meta"
6 | }
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": [
4 | "node_modules",
5 | "**/*.spec.ts",
6 | "**/*.spec.tsx",
7 | "**/*.test.ts",
8 | "**/*.test.tsx",
9 | ]
10 | }
--------------------------------------------------------------------------------
/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "sourceMap": false,
6 | "outDir": ".next/production-server/"
7 | },
8 | "include": ["server/**/*.ts"]
9 | }
--------------------------------------------------------------------------------
/lang/en/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "meta.title": "Hello World - This is Next.js boilerplate",
3 | "meta.description": "This is Next.js to easy development",
4 | "meta.keywords": "next.js, typescript, react, styled-components",
5 | "meta": "meta"
6 | }
--------------------------------------------------------------------------------
/sagas/index.ts:
--------------------------------------------------------------------------------
1 | import { all } from 'redux-saga/effects'
2 | import sample from './sample';
3 |
4 | const mergedSagas = [].concat(
5 | sample,
6 | );
7 |
8 | function * rootSaga () {
9 | yield all(mergedSagas)
10 | }
11 |
12 | export default rootSaga
--------------------------------------------------------------------------------
/server/api/sample/controller.ts:
--------------------------------------------------------------------------------
1 | import service from "../service";
2 |
3 | export const api = (req, res) => {
4 |
5 | const options = {
6 | method: 'get',
7 | uri: `/shows/1/episodes?specials=1`,
8 | params: req.query,
9 | };
10 |
11 | service(options, res)
12 |
13 | };
--------------------------------------------------------------------------------
/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux-immutable';
2 | import {Map} from 'immutable'
3 | import sample, { initialState as sampleState } from './sample';
4 | export const rootInitialState = Map({
5 | sample: sampleState,
6 | });
7 |
8 | export default combineReducers({
9 | sample,
10 | });
--------------------------------------------------------------------------------
/client/api/service.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Client
3 | */
4 |
5 | import axios from 'axios';
6 |
7 | export default ({uri, method, params = null, data = null, headers = null}) => {
8 | return axios({
9 | url: `${process.env.URL}/api${uri}`,
10 | method: method,
11 | params: params,
12 | data: data,
13 | headers: headers
14 | })
15 | };
--------------------------------------------------------------------------------
/static/etc/hello.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 이벤트용 정적페이지 서빙 테스트
6 |
7 |
8 |
9 |
10 |
11 | Hello!
12 |
13 |
14 |
--------------------------------------------------------------------------------
/lib/withIntl.ts:
--------------------------------------------------------------------------------
1 | import hoistNonReactStatics from 'hoist-non-react-statics'
2 | import { injectIntl } from 'react-intl'
3 |
4 | export const hoistStatics = (higherOrderComponent) => (BaseComponent) => {
5 | const NewComponent = higherOrderComponent(BaseComponent)
6 | hoistNonReactStatics(NewComponent, BaseComponent)
7 |
8 | return NewComponent
9 | }
10 |
11 | export default hoistStatics(injectIntl)
12 |
--------------------------------------------------------------------------------
/svgs/Sample.tsx:
--------------------------------------------------------------------------------
1 | const Sample = ({color = "#ffffff"}) => ()
4 |
5 | export default Sample;
--------------------------------------------------------------------------------
/ecosystem.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | apps : [{
3 | name: 'next',
4 | script: '.next/production-server/index.js',
5 | exec_mode: 'cluster',
6 | instances: 0, //0으로 지정 할 경우 cpu코어 수 만큼 인스턴스 생성
7 | autorestart: true,
8 | watch: false,
9 | max_memory_restart: '1G',
10 | env: {
11 | NODE_ENV: 'development'
12 | },
13 | env_production: {
14 | NODE_ENV: 'production',
15 | }
16 | }],
17 | };
18 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const TEST_REGEX = '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|js?|tsx?|ts?)$'
2 |
3 | module.exports = {
4 | setupFiles: ['/jest.setup.js'],
5 | testRegex: TEST_REGEX,
6 | transform: {
7 | '^.+\\.tsx?$': 'babel-jest'
8 | },
9 | testPathIgnorePatterns: [
10 | '/.next/', '/node_modules/'
11 | ],
12 | moduleFileExtensions: [
13 | 'ts', 'tsx', 'js', 'jsx'
14 | ],
15 | collectCoverage: true
16 | }
17 |
--------------------------------------------------------------------------------
/constants/index.ts:
--------------------------------------------------------------------------------
1 | //media query size
2 | export const $GRID_BREAKPOINTS_SM = 420;
3 | export const $GRID_BREAKPOINTS_MD = 768;
4 |
5 | //font
6 | export const $FONT_MALGUN = `"맑은 고딕", "Malgun Gothic", "돋움", "Dotum", sans-serif`;
7 | export const $FONT_OPEN_SANS = `"Open Sans", "맑은 고딕", "Malgun Gothic", sans-serif`;
8 |
9 | //color
10 | export const $BASE_COLOR = "#000000";
11 | export const $BASE_FONT_SIZE = 13;
12 | export const $BASE_LINE_HEIGHT = 1.45;
13 | export const $BASE_LETTER_SPACING = 0;
14 |
15 |
--------------------------------------------------------------------------------
/client/polyfills.js:
--------------------------------------------------------------------------------
1 |
2 | /* eslint no-extend-native: 0 */
3 | // core-js comes with Next.js. So, you can import it like below
4 | // import includes from 'core-js/library/fn/string/virtual/includes'
5 | // import repeat from 'core-js/library/fn/string/virtual/repeat'
6 | import '@babel/polyfill';
7 |
8 | // Add your polyfills
9 | // This files runs at the very beginning (even before React and Next.js core)
10 | console.log('Load your polyfills')
11 |
12 | // String.prototype.includes = includes
13 | // String.prototype.repeat = repeat
--------------------------------------------------------------------------------
/sagas/sample.ts:
--------------------------------------------------------------------------------
1 | import {call, put, take, fork} from 'redux-saga/effects'
2 | import * as api from '../client/api'
3 | import { actionTypes, sampleApiSuccess, sampleApiFailure } from '../actions/sample'
4 |
5 | function * sampleApiSaga () {
6 | while(true) {
7 | yield take(actionTypes.SAMPLE_API_REQUEST);
8 | try {
9 | const res = yield call(api.sampleApi);
10 | yield put(sampleApiSuccess(res))
11 | } catch (err) {
12 | yield put(sampleApiFailure(err))
13 | }
14 | }
15 | }
16 |
17 | export default [
18 | fork(sampleApiSaga)
19 | ]
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Dependency directories
9 | node_modules/
10 |
11 | # Optional npm cache directory
12 | .npm
13 |
14 | # locking file
15 | package-lock.json
16 | yarn.lock
17 |
18 | # dotenv environment variables file
19 | .env
20 |
21 | # next.js build output
22 | .next
23 | out/
24 |
25 | # vscode
26 | .vscode
27 |
28 | # jet brains idea
29 | .idea
30 |
31 | #cache
32 | .cache
33 | lang/.messages
34 |
35 | #source map
36 | *.map
37 |
38 | #typescript compiled server file
39 | server/**/*.js
40 |
41 | #jest test coverage
42 | coverage
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Dependency directories
9 | node_modules/
10 | .idea/
11 |
12 | # Optional npm cache directory
13 | .npm
14 |
15 | # locking file
16 | package-lock.json
17 | yarn.lock
18 |
19 | # dotenv environment variables file
20 | .env
21 |
22 | # next.js build output
23 | .next
24 | out/
25 |
26 | # vscode
27 | .vscode
28 |
29 | # jet brains idea
30 | .idea
31 |
32 | #cache
33 | .cache
34 | lang/.messages
35 |
36 | #source map
37 | *.map
38 |
39 | #typescript compiled server file
40 | server/**/*.js
41 |
42 | #jest test coverage
43 | coverage
--------------------------------------------------------------------------------
/actions/sample.ts:
--------------------------------------------------------------------------------
1 | export const actionTypes = {
2 | SAMPLE_API_REQUEST: 'SAMPLE_API_REQUEST',
3 | SAMPLE_API_SUCCESS: 'SAMPLE_API_SUCCESS',
4 | SAMPLE_API_FAILURE: 'SAMPLE_API_FAILURE',
5 | };
6 |
7 | export function sampleApiRequest () {
8 | return {
9 | type: actionTypes.SAMPLE_API_REQUEST,
10 | }
11 | }
12 |
13 | export function sampleApiSuccess (res) {
14 | return {
15 | type: actionTypes.SAMPLE_API_SUCCESS,
16 | res
17 | }
18 | }
19 |
20 | export function sampleApiFailure (err) {
21 | return {
22 | type: actionTypes.SAMPLE_API_FAILURE,
23 | err
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "target": "esnext",
5 | "module": "esnext",
6 | "jsx": "preserve",
7 | "allowJs": true,
8 | "moduleResolution": "node",
9 | "allowSyntheticDefaultImports": true,
10 | "noUnusedLocals": true,
11 | "noUnusedParameters": true,
12 | "removeComments": false,
13 | "preserveConstEnums": true,
14 | "sourceMap": true,
15 | "skipLibCheck": true,
16 | "baseUrl": ".",
17 | "typeRoots": [
18 | "./node_modules/@types"
19 | ],
20 | "lib": [
21 | "dom",
22 | "es2015",
23 | "es2016",
24 | "es2017"
25 | ]
26 | }
27 | }
--------------------------------------------------------------------------------
/components/sample/Sample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { $themeColor } from '../../styles/theme'
4 |
5 | const Container = styled.div`
6 | padding: 5px 10px 6px;
7 | border-radius: 13px;
8 | color: ${$themeColor};
9 | font-size: 11px;
10 | text-align: center;
11 | `;
12 |
13 | interface ISample {
14 | txt: string;
15 | }
16 |
17 | class Sample extends React.Component {
18 | render() {
19 | const {txt} = this.props;
20 | return (
21 |
22 | {txt}
23 |
24 | )
25 | }
26 | }
27 |
28 | export default Sample;
--------------------------------------------------------------------------------
/server/api/service.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Server
3 | */
4 |
5 | import axios from 'axios';
6 |
7 | export default ({uri, method, params = null, data = null, headers = null}, res) => {
8 |
9 | const respond = (response) => {
10 | res.json(response.data);
11 | };
12 |
13 | const onNetworkError = (error) => {
14 | res.status(error.response.status).json({
15 | message: error.message
16 | });
17 | };
18 |
19 | axios({
20 | url: `${process.env.API_URL}${encodeURI(uri)}`,
21 | method: method,
22 | params: params,
23 | data: data,
24 | headers: headers
25 | }).then(respond).catch(onNetworkError);
26 |
27 | };
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "next/babel",
4 | "@zeit/next-typescript/babel"
5 | ],
6 | "plugins": [
7 | "styled-components",
8 | "inline-react-svg"
9 | ],
10 | "env": {
11 | "development": {
12 | "plugins": [
13 | "react-intl"
14 | ]
15 | },
16 | "production": {
17 | "plugins": [
18 | "react-intl"
19 | ]
20 | },
21 | "test": {
22 | "presets": [
23 | [
24 | "next/babel",
25 | {
26 | "preset-env": {
27 | "modules": "commonjs"
28 | }
29 | }
30 | ],
31 | "@zeit/next-typescript/babel"
32 | ]
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/pages/_error.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled, { css } from 'styled-components'
3 | import { $themeColor, $themeCommon } from '../styles/theme'
4 | import { trackerOopsVisit } from '../lib/tracker'
5 |
6 | const Container = styled.div``
7 |
8 | export default class Error extends React.Component {
9 | static getInitialProps({ res, err }) {
10 | const statusCode = res ? res.statusCode : err ? err.statusCode : null
11 | return { statusCode }
12 | }
13 | render() {
14 | const { statusCode }: any = this.props
15 | return (
16 |
17 | Error!
18 | {statusCode}
19 |
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/styles/theme.ts:
--------------------------------------------------------------------------------
1 | import theme from 'styled-theming';
2 |
3 | //공통
4 | export const $themeCommon = {
5 | //state
6 | plus : '#2bb015',
7 | minus : '#bb200f',
8 | record :'#2887ff',
9 | };
10 |
11 | export const $themeColor = theme('theme', {
12 | light: "#31313c",
13 | dark: "#ffffff"
14 | });
15 |
16 | export const $themeColor2 = theme('theme', {
17 | light: "#ffffff",
18 | dark: "#31313c"
19 | });
20 |
21 | export const $themeBgDoc = theme('theme', {
22 | light: "#f0f0f0",
23 | dark: "#1c1c1f"
24 | });
25 | export const $themeFont = theme('theme', {
26 | light: "#43e4e6",
27 | dark: "#dee623"
28 | });
29 | export const $themeBg = theme('theme', {
30 | light: "#1318f0",
31 | dark: "#e66e01"
32 | });
--------------------------------------------------------------------------------
/store/index.ts:
--------------------------------------------------------------------------------
1 | import {createStore, applyMiddleware} from 'redux'
2 | import createSagaMiddleware from 'redux-saga'
3 | import rootReducers, {rootInitialState} from '../reducers'
4 | import rootSaga from '../sagas'
5 |
6 | const sagaMiddleware = createSagaMiddleware()
7 |
8 | const bindMiddleware = (middleware) => {
9 | if (process.env.NODE_ENV !== 'production') {
10 | const { composeWithDevTools } = require('redux-devtools-extension')
11 | return composeWithDevTools(applyMiddleware(...middleware))
12 | }
13 | return applyMiddleware(...middleware)
14 | }
15 |
16 | function configureStore (initialState = rootInitialState) {
17 | const store = createStore(
18 | rootReducers,
19 | initialState,
20 | bindMiddleware([sagaMiddleware])
21 | )
22 |
23 | store.runSagaTask = () => {
24 | store.sagaTask = sagaMiddleware.run(rootSaga)
25 | }
26 |
27 | store.runSagaTask()
28 | return store
29 | }
30 |
31 | export default configureStore
--------------------------------------------------------------------------------
/pages/param.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 | import { $themeColor, $themeColor2, $themeCommon } from '../styles/theme'
4 | import Layout, {ILayout} from '../components/Layout'
5 |
6 | const Container = styled.div``
7 | const H2 = styled.h2`
8 | background-color: ${$themeColor};
9 | color: ${$themeColor2};
10 | `
11 | const ParamValue = styled.h2`
12 | background-color: ${$themeCommon.record};
13 | color: #ffffff;
14 | `
15 |
16 | interface IParam extends ILayout {
17 | something: string;
18 | }
19 |
20 | class Param extends Component {
21 | static getInitialProps({ ctx }) {
22 | const { query } = ctx
23 | const { something } = query
24 | return { something }
25 | }
26 | render() {
27 | const { something } = this.props;
28 | return (
29 |
30 |
31 | Param Page
32 |
33 | something : {something}
34 |
35 |
36 |
37 | )
38 | }
39 | }
40 |
41 | export default Param
42 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 | import Layout, {ILayout} from '../components/Layout'
4 | import { $themeColor, $themeColor2 } from '../styles/theme'
5 | import * as sampleActions from '../actions/sample'
6 | import Sample from '../containers/Sample'
7 |
8 | const Container = styled.div``
9 | const H2 = styled.h2`
10 | background-color: ${$themeColor};
11 | color: ${$themeColor2};
12 | `
13 |
14 | interface IIndex extends ILayout {
15 | common_referrer: string;
16 | }
17 |
18 | class Index extends Component {
19 | static getInitialProps({ ctx }) {
20 | const { store } = ctx
21 | const state = store.getState();
22 | const sample_api_loaded = state.getIn(['sample', 'sample_api', 'loaded']);
23 | if(!sample_api_loaded) {
24 | store.dispatch(sampleActions.sampleApiRequest())
25 | }
26 | }
27 | render() {
28 | return (
29 |
30 |
31 | Index Page
32 |
33 |
34 |
35 | )
36 | }
37 | }
38 |
39 | export default Index
40 |
--------------------------------------------------------------------------------
/components/sample/__tests__/Login.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {shallow} from 'enzyme';
3 |
4 | import Login from './../Login';
5 |
6 | describe('Login', () => {
7 | it('renders the h1 title', () => {
8 | const login = shallow();
9 | expect(login.find('h1').text()).toEqual('Login');
10 | });
11 |
12 | it('renders the form', () => {
13 | const login = shallow();
14 | expect(login.find('form')).toHaveLength(1);
15 | });
16 |
17 | it('changes the text of email', () => {
18 | const login = shallow();
19 | login
20 | .find('#formEmail')
21 | .simulate('change', {
22 | target: {
23 | name: 'email',
24 | value: 'some@test.com'
25 | }
26 | });
27 | expect(login.update().find('#formEmail').props().value).toEqual('some@test.com');
28 | });
29 |
30 | it('changes the text of login button after clicking it', () => {
31 | const login = shallow();
32 | login
33 | .find('#loginSubmit')
34 | .simulate('click', {preventDefault() {}});
35 | expect(login.update().find('#loginSubmit').text()).toEqual('Logging in...');
36 | });
37 | });
--------------------------------------------------------------------------------
/reducers/sample.ts:
--------------------------------------------------------------------------------
1 | import {actionTypes} from '../actions/sample'
2 | import { fromJS, Map } from 'immutable'
3 |
4 | export const initialState = Map({
5 | sample_api: Map({
6 | status: 0,
7 | loading: false,
8 | loaded: false,
9 | data: Map({})
10 | }),
11 | })
12 |
13 | function reducer (state = initialState, action) {
14 | switch (action.type) {
15 | case actionTypes.SAMPLE_API_REQUEST:
16 | return state
17 | .setIn(["sample_api", "loading"], true)
18 | .setIn(["sample_api", "loaded"], false);
19 | case actionTypes.SAMPLE_API_SUCCESS:
20 | return state
21 | .setIn(["sample_api", "status"], action.res.status)
22 | .setIn(["sample_api", "loading"], false)
23 | .setIn(["sample_api", "loaded"], true)
24 | .setIn(["sample_api", "data"], fromJS(action.res.data));
25 | case actionTypes.SAMPLE_API_FAILURE:
26 | return state
27 | .setIn(["sample_api", "status"], action.err.response.status)
28 | .setIn(["sample_api", "loading"], false)
29 | .setIn(["sample_api", "loaded"], true)
30 | .setIn(["sample_api", "error"], fromJS(action.err.response))
31 | default:
32 | return state
33 | }
34 | }
35 |
36 | export default reducer
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const Dotenv = require('dotenv-webpack');
3 | const withTypescript = require('@zeit/next-typescript');
4 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
5 |
6 | const dev = process.env.NODE_ENV !== "production";
7 | const {CDN_URL} = process.env;
8 |
9 |
10 |
11 | module.exports = withTypescript({
12 | assetPrefix: CDN_URL,
13 | webpack: function(config) {
14 | config.plugins = config.plugins || []
15 |
16 | config.plugins = [
17 | ...config.plugins,
18 |
19 | // Read the .env file
20 | new Dotenv({
21 | path: path.join(__dirname, '.env'),
22 | systemvars: true
23 | }),
24 |
25 | !dev && new UglifyJsPlugin({
26 | parallel: true,
27 | uglifyOptions: {
28 | compress: {
29 | warnings: false,
30 | drop_console: true
31 | }
32 | },
33 | sourceMap: false
34 | })
35 | ].filter(plugin => plugin)
36 |
37 | const originalEntry = config.entry
38 | config.entry = async () => {
39 | const entries = await originalEntry()
40 |
41 | if (entries['main.js'] && !entries['main.js'].includes('./client/polyfills.js')) {
42 | entries['main.js'].unshift('./client/polyfills.js')
43 | }
44 |
45 | return entries
46 | }
47 |
48 | return config;
49 | }
50 | });
--------------------------------------------------------------------------------
/components/SelectLanguage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List, Map } from 'immutable'
3 | import Cookies from 'js-cookie';
4 |
5 | interface ISelectLanguage {
6 | lang: string;
7 | }
8 |
9 | class SelectLanguage extends React.Component {
10 | state = {
11 | languages: List([
12 | Map({
13 | id: 'KO',
14 | value: 'ko',
15 | text: '한국어'
16 | }),
17 | Map({
18 | id: 'EN',
19 | value: 'en',
20 | text: 'English'
21 | })
22 | ])
23 | }
24 | onChange = (e) => {
25 | const lang = e.target.value
26 | Cookies.set('lang', lang);
27 | window.location.reload()
28 | }
29 | render() {
30 | const {
31 | languages
32 | } = this.state;
33 | const {
34 | lang
35 | } = this.props;
36 | const current_language = languages.find(v => v.get('value') === lang).get('value')
37 | return (
38 |
45 | );
46 | }
47 | }
48 |
49 | export default SelectLanguage;
--------------------------------------------------------------------------------
/pages/other.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 | import { $themeBg, $themeColor, $themeColor2, $themeCommon, $themeFont } from '../styles/theme'
4 | import Layout, {ILayout} from '../components/Layout'
5 | import withIntl from '../lib/withIntl'
6 | import { InjectedIntlProps } from "react-intl"
7 | import Sample from '../containers/Sample'
8 |
9 | const Container = styled.div``
10 | const H2 = styled.h2`
11 | background-color: ${$themeColor};
12 | color: ${$themeColor2};
13 | `
14 | const TransContent = styled.div`
15 | background-color: ${$themeCommon.plus};
16 | color: yellow;
17 | `
18 | const ThemeColorComp = styled.div`
19 | background-color: ${$themeBg};
20 | color: ${$themeFont};
21 | `
22 |
23 | interface IOther extends ILayout, InjectedIntlProps {}
24 |
25 | class Other extends Component {
26 | static getInitialProps() {}
27 | render() {
28 | const {intl} = this.props;
29 | return (
30 |
31 |
32 | Other Page
33 | {intl.formatMessage({id: "sample.boilerplate"})}
34 | 1234567890
35 |
36 |
37 |
38 | )
39 | }
40 | }
41 |
42 | export default withIntl(Other)
43 |
--------------------------------------------------------------------------------
/pages/path/depth.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 | import { $themeColor, $themeColor2 } from '../../styles/theme'
4 | import Layout, {ILayout} from '../../components/Layout'
5 | import { connect } from 'react-redux'
6 | import * as sampleActions from '../../actions/sample'
7 | import {Map} from 'immutable';
8 | import Sample from '../../containers/Sample'
9 |
10 | const Container = styled.div``
11 | const H2 = styled.h2`
12 | background-color: ${$themeColor};
13 | color: ${$themeColor2};
14 | `
15 |
16 | interface IDepth extends ILayout {
17 | sample_api_loaded: Map;
18 | sampleApiRequest(): void;
19 | }
20 |
21 | class Depth extends Component {
22 | static getInitialProps() {}
23 | componentDidMount() {
24 | const {sample_api_loaded, sampleApiRequest} = this.props;
25 | if(!sample_api_loaded) {
26 | sampleApiRequest();
27 | }
28 | }
29 | render() {
30 | return (
31 |
32 |
33 | Depth Page
34 |
35 |
36 |
37 | )
38 | }
39 | }
40 |
41 | const mapStateToProps = state => ({
42 | sample_api_loaded: state.getIn([
43 | 'sample',
44 | 'sample_api',
45 | 'loaded'
46 | ]),
47 | })
48 | const mapDispatchToProps = (dispatch) => ({
49 | sampleApiRequest: () => dispatch(sampleActions.sampleApiRequest()),
50 | });
51 |
52 | export default connect(
53 | mapStateToProps,
54 | mapDispatchToProps
55 | )(Depth)
56 |
--------------------------------------------------------------------------------
/contexts/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, {Component, createContext} from 'react';
2 | import { ThemeProvider as StyledComponentsThemeProvider } from "styled-components";
3 | import Cookies from 'js-cookie';
4 | import GlobalStyle from "../styles/global";
5 |
6 | const Context = createContext({
7 | state: {
8 | theme: 'light'
9 | },
10 | actions: {
11 | changeTheme: () => {}
12 | }
13 | });
14 |
15 |
16 | const {Provider, Consumer: ThemeConsumer} = Context;
17 |
18 | interface IThemeProvider {
19 | theme: string,
20 | }
21 |
22 | /**
23 | * @TODO: context api로 테마 사용하는 예제 추가, (작업하면서 언어 바꾸는 예제도 같이 할꺼임.)
24 | */
25 | class ThemeProvider extends Component {
26 | state = {
27 | theme: this.props.theme
28 | }
29 | actions = {
30 | changeTheme: () => {
31 | const theme = this.state.theme !== 'light' ? 'light' : 'dark';
32 | Cookies.set('theme', theme);
33 | this.setState({theme})
34 | },
35 | }
36 | componentDidMount() {
37 | const {theme} = this.props;
38 | this.setState({theme})
39 | }
40 | render() {
41 | const {state, actions} = this;
42 | const value = {state, actions};
43 | const {theme} = state;
44 |
45 | return (
46 |
47 | <>
48 |
49 | {this.props.children}
50 | >
51 |
52 | )
53 | }
54 | }
55 |
56 | export default ThemeProvider
57 | export {ThemeConsumer}
--------------------------------------------------------------------------------
/containers/Sample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import withIntl from '../lib/withIntl'
3 | import { connect } from 'react-redux'
4 | import * as sampleActions from '../actions/sample'
5 | import { Map } from 'immutable'
6 | import styled from 'styled-components'
7 |
8 | const Button = styled.button.attrs({type: "button"})`
9 | padding: 14px;
10 | font-size: 18px;
11 | font-weight: bold;
12 | background-color: #7f7f7f;
13 | color: #ffffff;
14 |
15 | `
16 |
17 | interface ISample {
18 | sample_api: Map;
19 | sampleApiRequest(): void;
20 | }
21 |
22 | class Sample extends React.Component {
23 | render() {
24 | const {
25 | sample_api,
26 | sampleApiRequest
27 | } = this.props;
28 | const sample_api_loading = sample_api.get('loading');
29 | const sample_api_data = sample_api.get('data');
30 | return (
31 |
32 |
33 |
34 |
35 | {sample_api_loading
36 | ? `loading...`
37 | : `data: ${JSON.stringify(sample_api_data.toJS())}`
38 | }
39 |
40 | );
41 | }
42 | }
43 |
44 | const mapStateToProps = state => ({
45 | sample_api: state.getIn([
46 | 'sample',
47 | 'sample_api',
48 | ]),
49 | })
50 | const mapDispatchToProps = (dispatch) => ({
51 | sampleApiRequest: () => dispatch(sampleActions.sampleApiRequest()),
52 | });
53 |
54 | export default withIntl(connect(
55 | mapStateToProps,
56 | mapDispatchToProps
57 | )(Sample))
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Document, {Head, Main, NextScript} from 'next/document'
3 | import { ServerStyleSheet } from 'styled-components'
4 |
5 | interface IDocument {
6 | lang: string;
7 | locale: string;
8 | styleTags
9 | localeDataScript: string;
10 | }
11 |
12 | const {CDN_URL} = process.env;
13 |
14 | export default class extends Document {
15 | static async getInitialProps (context) {
16 | const sheet = new ServerStyleSheet()
17 | const props = await super.getInitialProps(context)
18 | const {req: {lang, localeDataScript}, renderPage} = context
19 | const page = renderPage(App => props => sheet.collectStyles())
20 | const styleTags = sheet.getStyleElement()
21 |
22 | return {
23 | ...props,
24 | ...page,
25 | styleTags,
26 | lang,
27 | localeDataScript
28 | }
29 | }
30 |
31 | render () {
32 | const {lang} = this.props;
33 | const polyfill = `https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.${lang}`
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {this.props.styleTags}
44 |
45 |
46 |
47 |
48 |
53 |
54 |
55 |
56 | )
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/components/sample/Login.tsx:
--------------------------------------------------------------------------------
1 | import React, {ChangeEvent} from 'react';
2 | import Router from 'next/router';
3 |
4 | import * as T from './types';
5 |
6 | export interface LoginProps {}
7 |
8 | export interface LoginState {
9 | credentials : T.LoginCredentials;
10 | isLoginLoading : boolean;
11 | }
12 |
13 | export default class Login extends React.Component < LoginProps,
14 | LoginState > {
15 | constructor(props : LoginProps) {
16 | super(props);
17 |
18 | this.state = {
19 | isLoginLoading: false,
20 | credentials: {
21 | email: null,
22 | password: null
23 | }
24 | }
25 | }
26 |
27 | handleCredentialsChange = (e : ChangeEvent < HTMLInputElement >) => {
28 | let {credentials} = this.state;
29 | credentials[e.target.name] = e.target.value;
30 |
31 | this.setState({credentials});
32 | }
33 |
34 | handleLoginSubmit = (e : React.MouseEvent < HTMLElement >) => {
35 | e.preventDefault();
36 | this.setState({isLoginLoading: true});
37 |
38 | setTimeout(() => {
39 | this.setState({isLoginLoading: false});
40 | Router.replace('/cars');
41 | }, 500);
42 | }
43 |
44 | render() {
45 | const {credentials} = this.state;
46 |
47 | return (
48 |
49 |
Login
50 |
67 |
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import App, { Container } from 'next/app'
2 | import React from 'react'
3 | import { IntlProvider, addLocaleData } from "react-intl";
4 | import NProgress from "nprogress";
5 | import Router from "next/router";
6 | import { Provider, connect } from 'react-redux'
7 | import withRedux from 'next-redux-wrapper'
8 | import withReduxSaga from 'next-redux-saga'
9 | import createStore from '../store'
10 | import { fromJS } from 'immutable'
11 | import ThemeProvider from "../contexts/ThemeProvider";
12 | import Cookies from 'js-cookie';
13 | import moment from "moment";
14 |
15 | declare global {
16 | interface Window {
17 | ReactIntlLocaleData: any;
18 | __NEXT_DATA__: any;
19 | }
20 | }
21 |
22 | if (typeof window !== "undefined" && window.ReactIntlLocaleData) {
23 | Object.keys(window.ReactIntlLocaleData).forEach(lang => {
24 | addLocaleData(window.ReactIntlLocaleData[lang]);
25 | });
26 | }
27 |
28 | Router.events.on("routeChangeStart", () => NProgress.start());
29 | Router.events.on("routeChangeComplete", () => NProgress.done());
30 | Router.events.on("routeChangeError", () => NProgress.done());
31 |
32 | interface IApp {
33 | store,
34 | locale: string;
35 | messages,
36 | lang: string;
37 | theme: string;
38 | chat: string;
39 | }
40 |
41 | class MyApp extends App {
42 | static async getInitialProps ({ Component, ctx }) {
43 | const pageProps = Component.getInitialProps ? await Component.getInitialProps({ ctx }) : {}
44 |
45 | const { req, isServer } = ctx;
46 | const { lang, locale, messages, theme } = req || window.__NEXT_DATA__.props.initialProps;
47 |
48 | return { pageProps, lang, locale, messages, theme, isServer };
49 | }
50 |
51 | componentDidMount() {
52 | const {lang} = this.props;
53 | Cookies.set("lang", lang);
54 | }
55 | render() {
56 | const { Component, pageProps, store, lang, locale, messages, theme } = this.props;
57 |
58 | moment.locale(lang);
59 |
60 | return (
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | );
71 | }
72 | }
73 |
74 | /**
75 | * immutable을 사용 할 경우 withRedux에 createStore, 뿐만 아니라 serializeState, deserializeState도 세팅해줘야 한다.
76 | */
77 | export default withRedux(createStore, {
78 | serializeState: state => state.toJS(),
79 | deserializeState: state => fromJS(state),
80 | })(withReduxSaga({ async: true })(connect()(MyApp)))
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-boilerplate",
3 | "version": "0.1.0",
4 | "description": "Next.js boilerplate project",
5 | "private": false,
6 | "repository": "https://github.com/richg0ld/nextjs-boilerplate.git",
7 | "keywords": [],
8 | "author": "richg0ld",
9 | "license": "ISC",
10 | "homepage": "https://github.com/richg0ld/nextjs-boilerplate#readme",
11 | "dependencies": {
12 | "accepts": "^1.3.5",
13 | "axios": "^0.18.0",
14 | "cookie-parser": "^1.4.3",
15 | "cross-env": "^5.2.0",
16 | "dotenv": "^6.1.0",
17 | "dotenv-webpack": "^1.5.7",
18 | "express": "^4.16.3",
19 | "glob": "^7.1.1",
20 | "hoist-non-react-statics": "^3.0.0-rc.1",
21 | "immutable": "^3.8.2",
22 | "js-cookie": "^2.2.0",
23 | "lru-cache": "^4.1.3",
24 | "moment": "^2.22.2",
25 | "morgan": "^1.9.1",
26 | "next": "^9.4.2",
27 | "next-redux-saga": "^3.0.0",
28 | "next-redux-wrapper": "^2.0.0",
29 | "nprogress": "^0.2.0",
30 | "pm2": "^3.2.2",
31 | "react": "^16.4.2",
32 | "react-dom": "^16.4.2",
33 | "react-intl": "^2.7.1",
34 | "react-redux": "^5.0.7",
35 | "react-tooltip": "^3.9.0",
36 | "redux": "^4.0.1",
37 | "redux-immutable": "^4.0.0",
38 | "redux-saga": "^0.16.2",
39 | "styled-components": "^4.0.0",
40 | "styled-theming": "^2.2.0",
41 | "ts-node": "^7.0.1",
42 | "typescript": "^3.1.3"
43 | },
44 | "scripts": {
45 | "test": "jest",
46 | "dev": "nodemon server/index.ts",
47 | "dev:prod": "pm2-dev start ecosystem.config.js",
48 | "build": "next build && tsc --project tsconfig.server.json",
49 | "start": "cross-env NODE_ENV=production node .next/production-server/index.js",
50 | "prod": "pm2-runtime start ecosystem.config.js --env production",
51 | "deploy": "yarn && pm2 kill && yarn build && pm2 start ecosystem.config.js --env production"
52 | },
53 | "devDependencies": {
54 | "@babel/polyfill": "^7.0.0",
55 | "@types/express": "^4.16.0",
56 | "@types/jest": "^23.0.0",
57 | "@types/next": "^7.0.2",
58 | "@types/node": "^10.11.7",
59 | "@types/react": "^16.4.16",
60 | "@types/react-dom": "^16.0.9",
61 | "@types/react-intl": "^2.3.15",
62 | "@zeit/next-typescript": "^1.1.0",
63 | "babel-core": "^7.0.0-bridge.0",
64 | "babel-jest": "23.0.1",
65 | "babel-plugin-inline-react-svg": "^1.0.1",
66 | "babel-plugin-react-intl": "^2.3.1",
67 | "babel-plugin-styled-components": "^1.8.0",
68 | "enzyme": "^3.3.0",
69 | "enzyme-adapter-react-16": "^1.1.1",
70 | "jest": "^23.1.0",
71 | "prettier": "1.15.2",
72 | "react-addons-test-utils": "^15.6.2",
73 | "react-test-renderer": "^16.2.0",
74 | "redux-devtools-extension": "^2.13.5",
75 | "uglifyjs-webpack-plugin": "^2.0.1"
76 | },
77 | "main": "server/index.ts"
78 | }
79 |
--------------------------------------------------------------------------------
/styles/mixin.ts:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components'
2 | import * as constants from '../constants'
3 |
4 | // Breakpoint viewport sizes and media queries.
5 | //
6 | // Breakpoints are defined as a map of (name: minimum width), order from small to large:
7 | //
8 | // (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)
9 | //
10 | // The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.
11 |
12 | // Name of the next breakpoint, or null for the last breakpoint.
13 | //
14 | // >> breakpoint-next(sm)
15 | // md
16 | // >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
17 | // md
18 | // >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))
19 | // md
20 | // @function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {
21 | // $n: index($breakpoint-names, $name);
22 | // @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);
23 | // }
24 | export const breakpointNext = (
25 | $name,
26 | $breakpoints = constants.$GRID_BREAKPOINTS,
27 | $breakpointNames = Object.keys($breakpoints)
28 | ) => {
29 | const $n = $breakpointNames.indexOf($name)
30 | return $n < $breakpointNames.length ? $breakpointNames[$n + 1] : null
31 | }
32 |
33 | // Minimum breakpoint width. Null for the smallest (first) breakpoint.
34 | //
35 | // >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
36 | // 576px
37 | // @function breakpoint-min($name, $breakpoints: $grid-breakpoints) {
38 | // $min: map-get($breakpoints, $name);
39 | // @return if($min != 0, $min, null);
40 | // }
41 | export const breakpointMin = ($name, $breakpoints = constants.$GRID_BREAKPOINTS) => {
42 | const $min = $breakpoints[$name]
43 | return $min != 0 ? $min : null
44 | }
45 |
46 | // Maximum breakpoint width. Null for the largest (last) breakpoint.
47 | // The maximum value is calculated as the minimum of the next one less 0.02px
48 | // to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.
49 | // See https://www.w3.org/TR/mediaqueries-4/#mq-min-max
50 | // Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.
51 | // See https://bugs.webkit.org/show_bug.cgi?id=178261
52 | //
53 | // >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
54 | // 767.98px
55 | // @function breakpoint-max($name, $breakpoints: $grid-breakpoints) {
56 | // $next: breakpoint-next($name, $breakpoints);
57 | // @return if($next, breakpoint-min($next, $breakpoints) - .02px, null);
58 | // }
59 | export const breakpointMax = ($name, $breakpoints = constants.$GRID_BREAKPOINTS) => {
60 | const $next = breakpointNext($name, $breakpoints)
61 | return $next ? breakpointMin($next, $breakpoints) - 0.02 : null
62 | }
63 |
64 | // Media of at most the maximum breakpoint width. No query for the largest breakpoint.
65 | // Makes the @content apply to the given breakpoint and narrower.
66 | // @mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {
67 | // $max: breakpoint-max($name, $breakpoints);
68 | // @if $max {
69 | // @media (max-width: $max) {
70 | // @content;
71 | // }
72 | // } @else {
73 | // @content;
74 | // }
75 | // }
76 | export const mediaBreakpointDown = (
77 | $name,
78 | $breakpoints = constants.$GRID_BREAKPOINTS,
79 | $content
80 | ) => {
81 | const $max = breakpointMax($name, $breakpoints)
82 | if ($max) {
83 | return css`
84 | @media (max-width: ${$max}px) {
85 | ${$content};
86 | }
87 | `
88 | } else {
89 | return css`
90 | ${$content}
91 | `
92 | }
93 | }
94 | // 자주쓰는 media 사이즈 커스텀 mixin
95 | export const mediaBreakpointDownSM = $content =>
96 | mediaBreakpointDown('$GRID_BREAKPOINTS_SM', undefined, $content)
--------------------------------------------------------------------------------
/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ReactNode } from 'react'
2 | import Head from 'next/head'
3 | import styled from "styled-components";
4 | import { injectIntl, InjectedIntlProps } from 'react-intl'
5 | import { $themeColor, $themeColor2 } from '../styles/theme'
6 | import Link from 'next/link'
7 | import Router from 'next/router'
8 | import {ThemeConsumer} from '../contexts/ThemeProvider'
9 | import SelectLanguage from './SelectLanguage'
10 |
11 | const Container = styled.article``;
12 | const Nav = styled.nav`
13 | background-color: ${$themeColor};
14 | color: ${$themeColor2};
15 | `;
16 | const Item = styled.li`
17 |
18 | `;
19 | const H1 = styled.h1`
20 | font-size: 50px;
21 | background-color: ${$themeColor};
22 | color: ${$themeColor2};
23 | `;
24 | const Form = styled.form`
25 | width: 200px;
26 | border: 1px solid #000000;
27 | `
28 | const ThemeBox = styled.span`
29 | color: black;
30 | background-color: yellowgreen;
31 | `
32 | const Input = styled.input.attrs({type: "text"})`
33 | font-size: 16px;
34 | `
35 |
36 | export interface ILayout extends InjectedIntlProps {
37 | title?: string;
38 | description?: string;
39 | keywords?: string;
40 | lang: string;
41 | children: ReactNode;
42 | }
43 |
44 | class Layout extends Component {
45 | state = {
46 | something: ''
47 | }
48 | onSubmit = (e) => {
49 | e.preventDefault()
50 | const { something } = this.state
51 | if (!something) return;
52 |
53 | return Router.push({
54 | pathname:'/param',
55 | query: {something}
56 | }, `/param/${something}`);
57 | }
58 | onChange = (e) => {
59 | this.setState({
60 | something: e.target.value,
61 | })
62 | }
63 | render() {
64 | const {
65 | something
66 | } = this.state;
67 | const {
68 | intl,
69 | title,
70 | description,
71 | keywords,
72 | lang,
73 | children,
74 | } = this.props;
75 |
76 | const _title = title || intl.formatMessage({id: "meta.title"});
77 | const _description = description || intl.formatMessage({id: "meta.description"});
78 | const _keywords = keywords || intl.formatMessage({id: "meta.keywords"});
79 |
80 | return (
81 |
82 |
83 | {_title}
84 |
85 |
86 |
87 |
88 |
89 |
90 | {({state, actions}) =>
91 |
92 | }
93 |
94 |
95 |
115 |
121 | {intl.formatMessage({id: "sample.helloWorld"})}
122 | {children}
123 |
124 | )
125 | }
126 | }
127 |
128 | export default injectIntl(Layout)
129 |
--------------------------------------------------------------------------------
/readme.MD:
--------------------------------------------------------------------------------
1 | # Next.js boilerplate
2 |
3 | 국내에는 next.js에 대한 예제를 많이 찾아볼 수 없어서. 이전 프로젝트에서 활용한 노하우를 통해 보일러플레이트를 만들었음. 처음 사용하거나 프로덕션에 적용하려는 분들에게 조금이나마 도움이 되었으면 좋겠음.
4 |
5 | # Navigation
6 |
7 | - [Spec](#Spec)
8 | - [Prerequisites](#Prerequisites)
9 | - [Pre-install](#Pre_Install)
10 | - [Config](#Config)
11 | - [Develop env](#Develop_env)
12 | - [Commands](#Commands)
13 | - [Start](#Start)
14 | - [Folder structure](#Folder_structure)
15 |
16 |
17 |
18 | # Spec
19 |
20 | - [o] next.js 세팅
21 | - [o] typescript (https://github.com/zeit/next.js/tree/canary/examples/custom-server-typescript, https://github.com/zeit/next.js/tree/canary/examples/with-typescript)
22 | - [o] 다국어 처리 (https://github.com/zeit/next.js/tree/canary/examples/with-react-intl)
23 | - [o] 다국시간 처리
24 | - [o] ie 브라우저 처리
25 | - [o] 템플릿 캐싱 (https://github.com/zeit/next.js/tree/canary/examples/ssr-caching)
26 | - [o] static 파일 설정 (도메인 변경) (https://github.com/zeit/next.js/tree/canary/examples/root-static-files)
27 | - [o] 환경변수 설정 (https://github.com/zeit/next.js/tree/canary/examples/with-dotenv)
28 | - [o] 쿠키 관리
29 | - [ ] 캐시 관리 (기능은 있으나 아직 활용을 못함.)
30 | - [o] 상태관리 (redux)
31 | - [o] Context api
32 | - [ ] GraphQL (추후 적용)
33 | - [o] 스타일링 styled-component ( https://github.com/zeit/next.js/tree/canary/examples/with-styled-components)
34 | - [o] 테마 작업 환경
35 | - [o] 페이지 이동시 로딩바(프로그레스바) (https://github.com/zeit/next.js/tree/canary/examples/with-loading)
36 | - [o] 로거환경
37 | - [o] immutable
38 | - [o] 커스텀 페이지(etc 페이지)
39 | - [o] tooltip
40 | - [o] test 환경 (jest, enzyme 으로 세팅 storybook은 nextjs환경에서의 문제인지 react16버전의 문제인지 정상작동 하지 않아서 포기)
41 | - [o] 폴리필 (ie10 <) 적용
42 | - [o] 프로세스 메니저 세팅
43 | - [o] svg 이미지 적용(색상 바꿀 수 있도록)
44 | - [o] 도커 세팅
45 |
46 |
47 | # Prerequisites
48 |
49 | - Node.js >= 8.x (Recommended the latest version) - 2018.08.08 v8.11.3 테스트 완료
50 |
51 | # Pre_Install
52 |
53 | 기본적으로 package.json에 등록된 npm명령어들은 yarn이 설치되었다는 전제하에 작성되어있다.
54 | yarn 설치 되어있다면 건너뛴다.
55 | ```sh
56 | npm i -g yarn
57 | ```
58 |
59 | # Install
60 |
61 | package.json에 작성된 의존성들을 설치한다.
62 | ```sh
63 | yarn
64 | ```
65 |
66 | # Config
67 |
68 | `.env.sample`파일을 복사하여 `.env`이름으로 새로 저장한다.
69 | ```sh
70 | cp .env.sample .env
71 | ```
72 |
73 | # Develop_env
74 |
75 | ```env
76 | URL=http://localhost:3000
77 | CDN_URL=
78 | PORT=
79 | API_URL=http://api.tvmaze.com //실제로 개발시에는 앞으로 사용 할 api주소를 사용해야한다.
80 | ```
81 |
82 | 포트 번호를 다르게 하고싶다면 URL 쪽 포트도 꼭 같도록 맞추어야 한다.
83 | ```env
84 | URL=http://localhost:8809
85 | CDN_URL=
86 | PORT=8809
87 | API_URL=http://api.tvmaze.com
88 | ```
89 |
90 | # Commands
91 |
92 | ```sh
93 | "scripts": {
94 | "test": "jest", //테스트코드
95 | "dev": "nodemon server/index.ts", //개발환경
96 | "dev:prod": "pm2-dev start ecosystem.config.js", //pm2 개발용 환경
97 | "build": "next build && tsc --project tsconfig.server.json", // 빌드
98 | "start": "cross-env NODE_ENV=production node .next/production-server/index.js", // 빌드코드 실행
99 | "prod": "pm2-runtime start ecosystem.config.js --env production", //pm2 프로덕션 실행
100 | "deploy": "yarn && pm2 kill && yarn build && pm2 start ecosystem.config.js --env production" // pm2로 배포
101 | }
102 | ```
103 |
104 | # Start
105 | `.env`파일에 `PORT`값을 기준으로 서버포트가 열린다. (아무값도 지정하지 않을 경우 :3000)
106 | ```sh
107 | yarn dev
108 | open http://localhost:3000
109 | ```
110 |
111 | # Folder_structure
112 | ```bash
113 | ├── README.md - 리드미 파일
114 | ├── tsconfig.test.json - 테스트 코드용 ts 설정 파일 (tsconfig.json을 확장하여 사용)
115 | ├── tsconfig.server.json - node 서버용 ts 설정 파일 (tsconfig.json을 확장하여 사용)
116 | ├── tsconfig.json - 기본 ts 설정 파일
117 | ├── package.json - npm 패키지 json
118 | ├── nodemon.json - nodemon 명령어용 json
119 | ├── next.config.js - nextjs 설정파일
120 | ├── ecosystem.config.js - pm2 설정파일
121 | ├── Dockerfile - docker 명령어 파일
122 | ├── .prettierrc - 프리티어 코드 포매팅 설정파일
123 | ├── .prettierignore - 코드 포매팅 제외 파일, 폴더 설정
124 | ├── .gitignore - git 제외 설정
125 | ├── .env.sample - .env 환경변수 샘플파일
126 | ├── .dockerignore - 도커 제외 설정
127 | ├── .babelrc - 바벨 컴파일러 설정
128 | │
129 | ├── svgs/ - svg components
130 | ├── styles/ - style용 함수, 변수들(styled-components 용)
131 | ├── store/ - 리덕스 스토어
132 | ├── static/ - img, font, css 등 static한 파일들
133 | ├── server/ - node 서버영역
134 | ├── sagas/ - 리덕스 사가
135 | ├── reducers/ - 리덕스 리듀서
136 | ├── pages/ - 실제 페이지용 컴포넌트 영역
137 | ├── lib/ - 자체 라이브러리
138 | ├── lang/ - 번역 정보 설정
139 | ├── contexts/ - react context api (테마 컨트롤, 채팅 레이아웃 컨트롤)
140 | ├── containers/ - container 컴포넌트 역역
141 | ├── constants/ - 상수영역
142 | ├── components/ - 컴포넌트 영역
143 | ├── client/ - client 영역
144 | │ └── api/ - client 용 api
145 | └── actions/ - 리덕스 actions
146 | ```
--------------------------------------------------------------------------------
/server/index.ts:
--------------------------------------------------------------------------------
1 | import {parse} from 'url'
2 | import {basename, join} from 'path'
3 | import * as dotenv from 'dotenv'
4 | import * as morgan from 'morgan'
5 | import * as cookieParser from 'cookie-parser'
6 | import * as LRUCache from 'lru-cache'
7 | import * as express from 'express'
8 | import * as accepts from 'accepts'
9 | import * as glob from 'glob'
10 | import * as next from 'next'
11 | import api from './api';
12 | import {readFileSync} from "fs";
13 |
14 | dotenv.config();
15 |
16 | const {PORT, NODE_ENV} = process.env;
17 | const port: number = parseInt(PORT, 10) || 3000
18 | const dev: boolean = NODE_ENV !== 'production'
19 | const app = next({dev})
20 | const handle = app.getRequestHandler()
21 | const server = express()
22 |
23 | /**
24 | * 서버쪽 ts코드들을 빌드할 때 .next/production-server/ 에 빌드가 된다. 해당 path에서 프로젝트 자원에 접근 하려면 개발환경 path와 depth가 차이 나기 때문에 아래코드로 환경에 따라 path경로를 변경시켜준다.
25 | */
26 | const env_path = dev ? '' : '../'
27 |
28 | const ssrCache = new LRUCache({
29 | max: 100,
30 | maxAge: 1000 * 60 * 60 // 1hour
31 | })
32 |
33 | const languages = glob.sync('./lang/*/').map((f) => basename(f))
34 | const statics = glob.sync('./static/**/*.*').map((f) => f.replace('./static', ''))
35 |
36 | const localeDataCache = new Map()
37 | const getLocaleDataScript = (locale) => {
38 | const lang = locale.split('-')[0]
39 | if (!localeDataCache.has(lang)) {
40 | const localeDataFile = require.resolve(`react-intl/locale-data/${lang}`)
41 | const localeDataScript = readFileSync(localeDataFile, 'utf8')
42 | localeDataCache.set(lang, localeDataScript)
43 | }
44 | return localeDataCache.get(lang)
45 | }
46 |
47 | const getMessages = (locale) => {
48 | return require(`./${env_path}../lang/${locale}`)
49 | }
50 |
51 | const getCacheKey = (req) => {
52 | return `${req.url}`
53 | }
54 |
55 | const renderAndCache = async function (req, res, pagePath, queryParams?) {
56 | if(true) {
57 | /**
58 | * 개발환경에서는 캐시 적용 안함.
59 | */
60 | app.render(req, res, pagePath, queryParams)
61 | return
62 | }
63 | const key = getCacheKey(req)
64 |
65 | // If we have a page in the cache, let's serve it
66 | if (ssrCache.has(key)) {
67 | console.log('HIT')
68 | res.setHeader('x-cache', 'HIT')
69 | res.send(ssrCache.get(key))
70 | return
71 | }
72 |
73 | try {
74 | // If not let's render the page into HTML
75 | const html = await app.renderToHTML(req, res, pagePath, queryParams)
76 |
77 | // Something is wrong with the request, let's skip the cache
78 | if (res.statusCode !== 200) {
79 | res.send(html)
80 | return
81 | }
82 |
83 | // Let's cache this page
84 | ssrCache.set(key, html)
85 | console.log('MISS')
86 | res.setHeader('x-cache', 'MISS')
87 | res.send(html)
88 | } catch (err) {
89 | app.renderError(err, req, res, pagePath, queryParams)
90 | }
91 | }
92 |
93 | const setCustomRequest = () => {
94 | return (req, _res, next) => {
95 | // Cookies that have been signed
96 | // console.log('Signed Cookies: ', req.signedCookies)
97 |
98 | const accept = accepts(req)
99 | const locale = accept.language(languages) || 'en'
100 | const lang = req.cookies['lang'] || locale
101 | const theme = req.cookies['theme'] || 'dark'
102 |
103 | req['theme'] = theme
104 | req['lang'] = lang
105 | req['locale'] = locale
106 | req['localeDataScript'] = getLocaleDataScript(lang)
107 | req['messages'] = getMessages(lang)
108 |
109 | next()
110 | }
111 | }
112 |
113 | app.prepare()
114 | .then(() => {
115 |
116 | server.use(cookieParser())
117 | server.use(morgan('dev'))
118 | server.use(setCustomRequest())
119 |
120 | // api
121 | server.use('/api', api);
122 |
123 | // Use the `renderAndCache` utility defined below to serve pages
124 | server.get('/', (req, res) => {
125 | renderAndCache(req, res, '/')
126 | });
127 |
128 | server.get('/param/:something', (req, res) => {
129 | const actualPage = '/param';
130 | const queryParams = {something: req.params.something};
131 | renderAndCache(req, res, actualPage, queryParams)
132 | });
133 |
134 | server.get('*', (req, res) => {
135 | const parsedUrl = parse(req.url, true)
136 |
137 | if (statics.indexOf(parsedUrl.pathname) > -1) {
138 | // static 파일 서빙
139 |
140 | const path = join(__dirname, `${env_path}../static`, parsedUrl.pathname)
141 | app.serveStatic(req, res, path)
142 | } else {
143 | return handle(req, res, parsedUrl);
144 | }
145 | });
146 |
147 | server.listen(port, (err) => {
148 | if(err) throw err;
149 | console.log(`'> Ready on http://localhost:${port}`);
150 | });
151 | })
152 | .catch((ex) => {
153 | console.error(ex.stack);
154 | process.exit(1);
155 | });
--------------------------------------------------------------------------------
/styles/global.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle, css } from 'styled-components';
2 | import {$BASE_COLOR, $BASE_FONT_SIZE, $BASE_LETTER_SPACING, $BASE_LINE_HEIGHT, $FONT_MALGUN, $FONT_OPEN_SANS} from "../constants";
3 | import { $themeBgDoc } from './theme'
4 |
5 | const GlobalStyle = createGlobalStyle`
6 | ${css`
7 | // font
8 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,700|Roboto:400,700);
9 | //맑은고딕 초기화
10 | @font-face {
11 | font-family: "맑은 고딕";
12 | src: local("AppleSDGothic"),
13 | local("Apple SD Gothic Neo"),
14 | local("AppleGothic"),
15 | local("맑은 고딕");
16 | }
17 | @font-face {
18 | font-family: "Malgun Gothic";
19 | src: local("AppleSDGothic"),
20 | local("Apple SD Gothic Neo"),
21 | local("AppleGothic"),
22 | local("Malgun Gothic");
23 | }
24 | @font-face {
25 | font-family: "Malgun Gothic";
26 | src: local("AppleSDGothic"),
27 | local("Apple SD Gothic Neo"),
28 | local("AppleGothic"),
29 | local("Malgun Gothic");
30 | }
31 | @font-face {
32 | font-family: "돋움";
33 | src: local("AppleSDGothic"),
34 | local("Apple SD Gothic Neo"),
35 | local("AppleGothic"),
36 | local("돋움");
37 | }
38 | @font-face {
39 | font-family: "Dotum";
40 | src: local("AppleSDGothic"),
41 | local("Apple SD Gothic Neo"),
42 | local("AppleGothic"),
43 | local("Dotum");
44 | }
45 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, main, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {margin:0; padding:0; border:0; word-break: keep-all;}
46 |
47 | //
48 | //
49 | //
50 | //
51 | //
52 |
53 | ol,
54 | ul,
55 | dl{ list-style: none;}
56 | hr{
57 | width:100%;
58 | height:1px;
59 | margin:0;
60 | padding:0;
61 | border:none;
62 | background-color: #000000;
63 | }
64 | em{
65 | font-style: normal;
66 | }
67 | mark{
68 | background-color: transparent;
69 | font-style: normal;
70 | }
71 | button{
72 | border:none;
73 | margin:0;
74 | padding:0;
75 | border-radius: 0;
76 | background-color: transparent;
77 | cursor: pointer;
78 | &:hover{ cursor:pointer;}
79 | }
80 | object{
81 | width:100%;
82 | vertical-align:top;
83 | }
84 |
85 | [lang|=ko]{ font-family: ${$FONT_MALGUN}; }
86 | [lang|=en]{ font-family: ${$FONT_OPEN_SANS}; }
87 | html{
88 | font-size: ${$BASE_FONT_SIZE}px;
89 | line-height: ${$BASE_LINE_HEIGHT};
90 | letter-spacing:${$BASE_LETTER_SPACING}px;
91 | -webkit-font-smoothing: antialiased;
92 | }
93 | body{
94 | background-color:${$themeBgDoc};
95 | color:${$BASE_COLOR};
96 | font-weight:400;
97 | }
98 |
99 | :lang(ko){ font-family: ${$FONT_MALGUN}; }
100 | :lang(en){ font-family: ${$FONT_OPEN_SANS}; }
101 |
102 | //
103 | //
104 | //
105 | //
106 | //
107 | //
108 | a{
109 | color:inherit;
110 | text-decoration:none;
111 | &:hover{ text-decoration: none;}
112 | }
113 | //
114 | //
115 | //
116 | //
117 | //
118 |
119 | img{
120 | max-width:100%;
121 | border: none;
122 | vertical-align: top;
123 | }
124 | //
125 | //
126 | //
127 | //
128 | //
129 |
130 | table{
131 | border-spacing: 0;
132 | border-collapse: collapse;
133 | }
134 | caption{font:0/0 a;}
135 | th,
136 | td{}
137 | th{
138 | font-weight:400;
139 | text-align:left;
140 | }
141 | td{}
142 |
143 | a:focus,
144 | input:focus,
145 | select:focus,
146 | textarea:focus {
147 | outline: 1px solid -webkit-focus-ring-color;
148 | outline-offset: -1px;
149 | }
150 |
151 | //
152 | //
153 | //
154 | //
155 | //
156 |
157 | input[type='number']::-webkit-inner-spin-button,
158 | input[type='number']::-webkit-outer-spin-button {height:auto;-webkit-appearance:none; margin:0;}
159 | input[type='search'] {-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}
160 | input[type='search']::-webkit-search-cancel-button, input[type='search']::-webkit-search-decoration {-webkit-appearance:none;}
161 | input[type="text"],
162 | input[type="password"],
163 | input[type="search"],
164 | input[type="tel"],
165 | input[type="date"],
166 | input[type="number"],
167 | input[type="email"]{box-sizing:border-box;width:100%;margin:0;padding:0; border:none; }
168 |
169 |
170 |
171 | /*
172 | 글로벌 css styled components로 대체 못하는 css들 여기다 작업
173 | */
174 | .popular-steamers .popular-steamer {
175 | padding: 0;
176 | transform: scale(0.7);
177 | transition: all .3s ease;
178 | margin: 0 -35px;
179 | }
180 | .popular-steamers .popular-steamer .popular-steamer-media {
181 | box-shadow: none;
182 | }
183 | .popular-steamers .popular-steamer .popular-steamer-header {
184 | visibility: hidden;
185 | }
186 | .popular-steamers .popular-steamer .popular-steamer-media-info-play,
187 | .popular-steamers .popular-steamer .popular-steamer-media-info-data-list,
188 | .popular-steamers .popular-steamer .popular-steamer-media-info-view {
189 | display: none;
190 | }
191 | .popular-steamers .slick-center .popular-steamer .popular-steamer-header {
192 | visibility: visible;
193 | }
194 | .popular-steamers .slick-center .popular-steamer .popular-steamer-media-info-play,
195 | .popular-steamers .slick-center .popular-steamer .popular-steamer-media-info-data-list,
196 | .popular-steamers .slick-center .popular-steamer .popular-steamer-media-info-view {
197 | display: block;
198 | }
199 | .popular-steamers .slick-center .popular-steamer .popular-steamer-media-info {
200 | left: -1px;
201 | width: 100.2%;
202 | }
203 | .popular-steamers .slick-center .popular-steamer {
204 | opacity: 1;
205 | transform: scale(1);
206 | }
207 | .popular-steamers .slick-center .popular-steamer .popular-steamer-media {
208 | box-shadow: 0 60px 60px 0 rgba(32, 24, 54, 0.72);
209 | }
210 | .common-tooltip strong {
211 | white-space: pre-wrap;
212 | color: #ffffff;
213 | font-family: OpenSans;
214 | font-size: 11px;
215 | }
216 | .common-tooltip div {
217 | white-space: pre-wrap;
218 | color: #9d9d9d;
219 | font-family: OpenSans;
220 | font-size: 11px;
221 | }
222 |
223 | /* Make clicks pass-through */
224 | #nprogress {
225 | pointer-events: none;
226 | }
227 |
228 | #nprogress .bar {
229 | background: hotpink;
230 |
231 | position: fixed;
232 | z-index: 1031;
233 | top: 0;
234 | left: 0;
235 |
236 | width: 100%;
237 | height: 2px;
238 | }
239 |
240 | /* Fancy blur effect */
241 | #nprogress .peg {
242 | display: block;
243 | position: absolute;
244 | right: 0px;
245 | width: 100px;
246 | height: 100%;
247 | box-shadow: 0 0 10px hotpink, 0 0 5px hotpink;
248 | opacity: 1.0;
249 |
250 | -webkit-transform: rotate(3deg) translate(0px, -4px);
251 | -ms-transform: rotate(3deg) translate(0px, -4px);
252 | transform: rotate(3deg) translate(0px, -4px);
253 | }
254 |
255 | /* Remove these to get rid of the spinner */
256 | #nprogress .spinner {
257 | display: block;
258 | position: fixed;
259 | z-index: 1031;
260 | top: 15px;
261 | right: 15px;
262 | }
263 |
264 | #nprogress .spinner-icon {
265 | width: 18px;
266 | height: 18px;
267 | box-sizing: border-box;
268 |
269 | border: solid 2px transparent;
270 | border-top-color: hotpink;
271 | border-left-color: hotpink;
272 | border-radius: 50%;
273 |
274 | -webkit-animation: nprogress-spinner 400ms linear infinite;
275 | animation: nprogress-spinner 400ms linear infinite;
276 | }
277 |
278 | .nprogress-custom-parent {
279 | overflow: hidden;
280 | position: relative;
281 | }
282 |
283 | .nprogress-custom-parent #nprogress .spinner,
284 | .nprogress-custom-parent #nprogress .bar {
285 | position: absolute;
286 | }
287 |
288 | @-webkit-keyframes nprogress-spinner {
289 | 0% { -webkit-transform: rotate(0deg); }
290 | 100% { -webkit-transform: rotate(360deg); }
291 | }
292 | @keyframes nprogress-spinner {
293 | 0% { transform: rotate(0deg); }
294 | 100% { transform: rotate(360deg); }
295 | }
296 |
297 | //브라우저 최소사이즈 때 생기는 여백잡는용
298 | html {
299 | min-width: 1280px;
300 | @media (max-width: 768px) {
301 | min-width: auto;
302 | }
303 | }
304 |
305 | `}
306 | `;
307 |
308 | export default GlobalStyle;
--------------------------------------------------------------------------------