├── .gitignore
├── README.md
├── game2048
├── final
│ ├── .gitignore
│ ├── jsconfig.json
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ └── src
│ │ ├── App.js
│ │ ├── component
│ │ ├── AboveGame.js
│ │ ├── Game.js
│ │ ├── Header.js
│ │ └── Tile.js
│ │ ├── constant.js
│ │ ├── hook
│ │ ├── useLocalStorageNumber.js
│ │ └── useMoveTile.js
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── setupTests.js
│ │ └── util
│ │ ├── __snapshots__
│ │ └── tile.test.js.snap
│ │ ├── assert.js
│ │ ├── keyboard.js
│ │ ├── number.js
│ │ ├── tile.js
│ │ └── tile.test.js
└── start
│ ├── .gitignore
│ ├── jsconfig.json
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
│ └── src
│ ├── App.js
│ ├── component
│ ├── AboveGame.js
│ ├── Game.js
│ └── Header.js
│ ├── index.css
│ ├── index.js
│ └── setupTests.js
├── ts-todo
├── final
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── Command.ts
│ │ ├── Input.ts
│ │ ├── Todo.ts
│ │ ├── index.ts
│ │ ├── type.ts
│ │ └── util.ts
│ └── tsconfig.json
└── start
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ ├── Input.ts
│ ├── index.ts
│ └── util.ts
│ └── tsconfig.json
└── whois
├── final
├── .env.development
├── .env.production
├── .gitignore
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── server
│ ├── data.db
│ ├── db.js
│ ├── index.js
│ ├── package-lock.json
│ └── package.json
└── src
│ ├── App.js
│ ├── auth
│ ├── component
│ │ └── AuthLayout.js
│ ├── container
│ │ ├── Login.js
│ │ └── Signup.js
│ ├── hook
│ │ └── useBlockLoginUser.js
│ └── state
│ │ ├── index.js
│ │ └── saga.js
│ ├── common
│ ├── component
│ │ └── History.js
│ ├── constant.js
│ ├── hook
│ │ ├── useFetchInfo.js
│ │ └── useNeedLogin.js
│ ├── redux-helper.js
│ ├── state
│ │ └── index.js
│ ├── store.js
│ └── util
│ │ ├── api.js
│ │ └── fetch.js
│ ├── index.js
│ ├── search
│ ├── component
│ │ └── Settings.js
│ ├── container
│ │ ├── Search.js
│ │ └── SearchInput.js
│ └── state
│ │ ├── index.js
│ │ └── saga.js
│ ├── setupTests.js
│ └── user
│ ├── component
│ └── FetchLabel.js
│ ├── container
│ ├── Department.js
│ ├── TagList.js
│ └── User.js
│ └── state
│ ├── index.js
│ └── saga.js
└── start
├── .env.development
├── .env.production
├── .gitignore
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── server
├── data.db
├── db.js
├── index.js
├── package-lock.json
└── package.json
└── src
├── App.js
├── common
├── constant.js
├── hook
│ └── useFetchInfo.js
├── redux-helper.js
├── state
│ └── index.js
├── store.js
└── util
│ ├── api.js
│ └── fetch.js
├── index.js
└── setupTests.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .next
2 | .storybookOutput
3 | .serverOutput
4 | .static
5 | build.zip
6 | yarn.lock
7 | node_modules
8 | nginxLocal/error.log
9 | nginxLocal/access.log
10 | nginxLocal/healthd.application.log*
11 | .DS_Store
12 | .vscode/chrome
13 | .vscode/launch.json
14 | stories/**/*.js
15 | access.log
16 | static/html/next
17 | out
18 | **/__pycache__
19 | .cache
20 | i18n/token.json
21 | logs
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 인프런 강의 프로젝트 코드입니다
2 |
3 | [실전 리액트 프로그래밍](https://www.inflearn.com/course/%EC%8B%A4%EC%A0%84-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D)
4 |
5 | [타입스크립트 시작하기](https://www.inflearn.com/course/%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0)
6 |
--------------------------------------------------------------------------------
/game2048/final/.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 |
--------------------------------------------------------------------------------
/game2048/final/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "module": "commonjs",
5 | "target": "es6",
6 | "checkJs": true
7 | },
8 | "exclude": ["node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/game2048/final/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "game2048-final",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.5.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "classnames": "^2.2.6",
10 | "hotkeys-js": "^3.8.1",
11 | "lodash": "^4.17.19",
12 | "react": "^16.13.1",
13 | "react-dom": "^16.13.1",
14 | "react-scripts": "3.4.1"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": "react-app"
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/game2048/final/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/game2048/final/public/favicon.ico
--------------------------------------------------------------------------------
/game2048/final/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/game2048/final/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/game2048/final/public/logo192.png
--------------------------------------------------------------------------------
/game2048/final/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/game2048/final/public/logo512.png
--------------------------------------------------------------------------------
/game2048/final/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/game2048/final/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/game2048/final/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Header from './component/Header';
3 | import AboveGame from './component/AboveGame';
4 | import Game from './component/Game';
5 | import useLocalStorageNumber from './hook/useLocalStorageNumber';
6 |
7 | export default function App() {
8 | const [score, setScore] = useState(0);
9 | const [bestScore, setBestScore] = useLocalStorageNumber('bestScore', 0);
10 |
11 | useEffect(() => {
12 | if (score > bestScore) {
13 | setBestScore(score);
14 | }
15 | });
16 |
17 | return (
18 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/game2048/final/src/component/AboveGame.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function AboveGame() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/game2048/final/src/component/Game.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import times from 'lodash/times';
3 | import useMoveTile from '../hook/useMoveTile';
4 | import { getInitialTileList } from '../util/tile';
5 | import { MAX_POS } from '../constant';
6 | import Tile from './Tile';
7 |
8 | export default function Game({ setScore }) {
9 | const [tileList, setTileList] = useState(getInitialTileList);
10 | useMoveTile({ tileList, setTileList, setScore });
11 | return (
12 |
13 |
14 | {times(MAX_POS, y => (
15 |
16 | {times(MAX_POS, x => (
17 |
18 | ))}
19 |
20 | ))}
21 |
22 |
23 |
24 | {tileList.map(item => (
25 |
26 | ))}
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/game2048/final/src/component/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function Header({ score, bestScore }) {
4 | return (
5 |
6 | 2048
7 |
8 |
9 | {score}
10 |
11 |
{bestScore}
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/game2048/final/src/component/Tile.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cn from 'classnames';
3 |
4 | export default function Tile({ x, y, value, isMerged, isNew }) {
5 | return (
6 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/game2048/final/src/constant.js:
--------------------------------------------------------------------------------
1 | export const MAX_POS = 4;
2 |
--------------------------------------------------------------------------------
/game2048/final/src/hook/useLocalStorageNumber.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | export default function useLocalStorageNumber(key, initialValue) {
4 | const [value, setValue] = useState(initialValue);
5 |
6 | useEffect(() => {
7 | const valueStr = window.localStorage.getItem(key);
8 | if (valueStr) {
9 | setValue(Number(valueStr));
10 | }
11 | }, [key]);
12 |
13 | useEffect(() => {
14 | const prev = window.localStorage.getItem(key);
15 | const next = String(value);
16 | if (prev !== next) {
17 | window.localStorage.setItem(key, next);
18 | }
19 | }, [key, value]);
20 |
21 | return [value, setValue];
22 | }
23 |
--------------------------------------------------------------------------------
/game2048/final/src/hook/useMoveTile.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { makeTile, moveTile } from '../util/tile';
3 | import { addKeyCallback, removeKeyCallback } from '../util/keyboard';
4 |
5 | export default function useMoveTile({ tileList, setTileList, setScore }) {
6 | useEffect(() => {
7 | function moveAndAdd({ x, y }) {
8 | const newTileList = moveTile({ tileList, x, y });
9 | const score = newTileList.reduce(
10 | (acc, item) => (item.isMerged ? acc + item.value : acc),
11 | 0,
12 | );
13 | setScore(v => v + score);
14 | const newTile = makeTile(newTileList);
15 | newTile.isNew = true;
16 | newTileList.push(newTile);
17 | setTileList(newTileList);
18 | }
19 |
20 | function moveUp() {
21 | moveAndAdd({ x: 0, y: -1 });
22 | }
23 | function moveDown() {
24 | moveAndAdd({ x: 0, y: 1 });
25 | }
26 | function moveLeft() {
27 | moveAndAdd({ x: -1, y: 0 });
28 | }
29 | function moveRight() {
30 | moveAndAdd({ x: 1, y: 0 });
31 | }
32 | addKeyCallback('up', moveUp);
33 | addKeyCallback('down', moveDown);
34 | addKeyCallback('left', moveLeft);
35 | addKeyCallback('right', moveRight);
36 | return () => {
37 | removeKeyCallback('up', moveUp);
38 | removeKeyCallback('down', moveDown);
39 | removeKeyCallback('left', moveLeft);
40 | removeKeyCallback('right', moveRight);
41 | };
42 | }, [tileList, setTileList, setScore]);
43 | }
44 |
--------------------------------------------------------------------------------
/game2048/final/src/index.css:
--------------------------------------------------------------------------------
1 | /* @import url(fonts/clear-sans.css); */
2 | html,
3 | body {
4 | margin: 0;
5 | padding: 0;
6 | background: #faf8ef;
7 | color: #776e65;
8 | font-family: 'Clear Sans', 'Helvetica Neue', Arial, sans-serif;
9 | font-size: 18px;
10 | }
11 |
12 | body {
13 | margin: 80px 0;
14 | }
15 |
16 | input {
17 | display: inline-block;
18 | background: #8f7a66;
19 | border-radius: 3px;
20 | padding: 0 20px;
21 | text-decoration: none;
22 | color: #f9f6f2;
23 | height: 40px;
24 | line-height: 42px;
25 | cursor: pointer;
26 | font: inherit;
27 | border: none;
28 | outline: none;
29 | box-sizing: border-box;
30 | font-weight: bold;
31 | margin: 0;
32 | -webkit-appearance: none;
33 | -moz-appearance: none;
34 | appearance: none;
35 | }
36 | input[type='text'],
37 | input[type='email'] {
38 | cursor: auto;
39 | background: #fcfbf9;
40 | font-weight: normal;
41 | color: #776e65;
42 | padding: 0 15px;
43 | }
44 | input[type='text']::-webkit-input-placeholder,
45 | input[type='email']::-webkit-input-placeholder {
46 | color: #9d948c;
47 | }
48 | input[type='text']::-moz-placeholder,
49 | input[type='email']::-moz-placeholder {
50 | color: #9d948c;
51 | }
52 | input[type='text']:-ms-input-placeholder,
53 | input[type='email']:-ms-input-placeholder {
54 | color: #9d948c;
55 | }
56 |
57 | .heading:after {
58 | content: '';
59 | display: block;
60 | clear: both;
61 | }
62 |
63 | h1.title {
64 | font-size: 80px;
65 | font-weight: bold;
66 | margin: 0;
67 | display: block;
68 | float: left;
69 | }
70 |
71 | @-webkit-keyframes move-up {
72 | 0% {
73 | top: 25px;
74 | opacity: 1;
75 | }
76 |
77 | 100% {
78 | top: -50px;
79 | opacity: 0;
80 | }
81 | }
82 | @-moz-keyframes move-up {
83 | 0% {
84 | top: 25px;
85 | opacity: 1;
86 | }
87 |
88 | 100% {
89 | top: -50px;
90 | opacity: 0;
91 | }
92 | }
93 | @keyframes move-up {
94 | 0% {
95 | top: 25px;
96 | opacity: 1;
97 | }
98 |
99 | 100% {
100 | top: -50px;
101 | opacity: 0;
102 | }
103 | }
104 | .scores-container {
105 | float: right;
106 | text-align: right;
107 | }
108 |
109 | .score-container,
110 | .best-container {
111 | position: relative;
112 | display: inline-block;
113 | background: #bbada0;
114 | padding: 15px 25px;
115 | font-size: 25px;
116 | height: 25px;
117 | line-height: 47px;
118 | font-weight: bold;
119 | border-radius: 3px;
120 | color: white;
121 | margin-top: 8px;
122 | text-align: center;
123 | }
124 | .score-container:after,
125 | .best-container:after {
126 | position: absolute;
127 | width: 100%;
128 | top: 10px;
129 | left: 0;
130 | text-transform: uppercase;
131 | font-size: 13px;
132 | line-height: 13px;
133 | text-align: center;
134 | color: #eee4da;
135 | }
136 | .score-container .score-addition,
137 | .best-container .score-addition {
138 | position: absolute;
139 | right: 30px;
140 | color: red;
141 | font-size: 25px;
142 | line-height: 25px;
143 | font-weight: bold;
144 | color: rgba(119, 110, 101, 0.9);
145 | z-index: 100;
146 | -webkit-animation: move-up 600ms ease-in;
147 | -moz-animation: move-up 600ms ease-in;
148 | animation: move-up 600ms ease-in;
149 | -webkit-animation-fill-mode: both;
150 | -moz-animation-fill-mode: both;
151 | animation-fill-mode: both;
152 | }
153 |
154 | .score-container:after {
155 | content: 'Score';
156 | }
157 |
158 | .best-container:after {
159 | content: 'Best';
160 | }
161 |
162 | p {
163 | margin-top: 0;
164 | margin-bottom: 10px;
165 | line-height: 1.65;
166 | }
167 |
168 | a {
169 | color: #776e65;
170 | font-weight: bold;
171 | text-decoration: underline;
172 | cursor: pointer;
173 | }
174 |
175 | strong.important {
176 | text-transform: uppercase;
177 | }
178 |
179 | hr {
180 | border: none;
181 | border-bottom: 1px solid #d8d4d0;
182 | margin-top: 20px;
183 | margin-bottom: 30px;
184 | }
185 |
186 | .container {
187 | width: 500px;
188 | margin: 0 auto;
189 | }
190 |
191 | @-webkit-keyframes fade-in {
192 | 0% {
193 | opacity: 0;
194 | }
195 |
196 | 100% {
197 | opacity: 1;
198 | }
199 | }
200 | @-moz-keyframes fade-in {
201 | 0% {
202 | opacity: 0;
203 | }
204 |
205 | 100% {
206 | opacity: 1;
207 | }
208 | }
209 | @keyframes fade-in {
210 | 0% {
211 | opacity: 0;
212 | }
213 |
214 | 100% {
215 | opacity: 1;
216 | }
217 | }
218 | @-webkit-keyframes slide-up {
219 | 0% {
220 | margin-top: 32%;
221 | }
222 |
223 | 100% {
224 | margin-top: 20%;
225 | }
226 | }
227 | @-moz-keyframes slide-up {
228 | 0% {
229 | margin-top: 32%;
230 | }
231 |
232 | 100% {
233 | margin-top: 20%;
234 | }
235 | }
236 | @keyframes slide-up {
237 | 0% {
238 | margin-top: 32%;
239 | }
240 |
241 | 100% {
242 | margin-top: 20%;
243 | }
244 | }
245 | .game-container {
246 | margin-top: 40px;
247 | position: relative;
248 | padding: 15px;
249 | cursor: default;
250 | -webkit-touch-callout: none;
251 | -ms-touch-callout: none;
252 | -webkit-user-select: none;
253 | -moz-user-select: none;
254 | -ms-user-select: none;
255 | -ms-touch-action: none;
256 | touch-action: none;
257 | background: #bbada0;
258 | border-radius: 6px;
259 | width: 500px;
260 | height: 500px;
261 | -webkit-box-sizing: border-box;
262 | -moz-box-sizing: border-box;
263 | box-sizing: border-box;
264 | }
265 |
266 | .game-message {
267 | display: none;
268 | position: absolute;
269 | top: 0;
270 | right: 0;
271 | bottom: 0;
272 | left: 0;
273 | background: rgba(238, 228, 218, 0.73);
274 | z-index: 100;
275 | padding-top: 40px;
276 | text-align: center;
277 | -webkit-animation: fade-in 800ms ease 1200ms;
278 | -moz-animation: fade-in 800ms ease 1200ms;
279 | animation: fade-in 800ms ease 1200ms;
280 | -webkit-animation-fill-mode: both;
281 | -moz-animation-fill-mode: both;
282 | animation-fill-mode: both;
283 | }
284 | .game-message p {
285 | font-size: 60px;
286 | font-weight: bold;
287 | height: 60px;
288 | line-height: 60px;
289 | margin-top: 222px;
290 | }
291 | .game-message .lower {
292 | display: block;
293 | margin-top: 29px;
294 | }
295 | .game-message .mailing-list {
296 | margin-top: 52px;
297 | }
298 | .game-message .mailing-list strong {
299 | display: block;
300 | margin-bottom: 10px;
301 | }
302 | .game-message .mailing-list .mailing-list-email-field {
303 | width: 230px;
304 | margin-right: 5px;
305 | }
306 | .game-message a {
307 | display: inline-block;
308 | background: #8f7a66;
309 | border-radius: 3px;
310 | padding: 0 20px;
311 | text-decoration: none;
312 | color: #f9f6f2;
313 | height: 40px;
314 | line-height: 42px;
315 | cursor: pointer;
316 | margin-left: 9px;
317 | }
318 | .game-message a.keep-playing-button {
319 | display: none;
320 | }
321 | .game-message .score-sharing {
322 | display: inline-block;
323 | vertical-align: middle;
324 | margin-left: 10px;
325 | }
326 | .game-message.game-won {
327 | background: rgba(237, 194, 46, 0.5);
328 | color: #f9f6f2;
329 | }
330 | .game-message.game-won a.keep-playing-button {
331 | display: inline-block;
332 | }
333 | .game-message.game-won,
334 | .game-message.game-over {
335 | display: block;
336 | }
337 | .game-message.game-won p,
338 | .game-message.game-over p {
339 | -webkit-animation: slide-up 1.5s ease-in-out 2500ms;
340 | -moz-animation: slide-up 1.5s ease-in-out 2500ms;
341 | animation: slide-up 1.5s ease-in-out 2500ms;
342 | -webkit-animation-fill-mode: both;
343 | -moz-animation-fill-mode: both;
344 | animation-fill-mode: both;
345 | }
346 | .game-message.game-won .mailing-list,
347 | .game-message.game-over .mailing-list {
348 | -webkit-animation: fade-in 1.5s ease-in-out 2500ms;
349 | -moz-animation: fade-in 1.5s ease-in-out 2500ms;
350 | animation: fade-in 1.5s ease-in-out 2500ms;
351 | -webkit-animation-fill-mode: both;
352 | -moz-animation-fill-mode: both;
353 | animation-fill-mode: both;
354 | }
355 |
356 | .grid-container {
357 | position: absolute;
358 | z-index: 1;
359 | }
360 |
361 | .grid-row {
362 | margin-bottom: 15px;
363 | }
364 | .grid-row:last-child {
365 | margin-bottom: 0;
366 | }
367 | .grid-row:after {
368 | content: '';
369 | display: block;
370 | clear: both;
371 | }
372 |
373 | .grid-cell {
374 | width: 106.25px;
375 | height: 106.25px;
376 | margin-right: 15px;
377 | float: left;
378 | border-radius: 3px;
379 | background: rgba(238, 228, 218, 0.35);
380 | }
381 | .grid-cell:last-child {
382 | margin-right: 0;
383 | }
384 |
385 | .tile-container {
386 | position: absolute;
387 | z-index: 2;
388 | }
389 |
390 | .tile,
391 | .tile .tile-inner {
392 | width: 107px;
393 | height: 107px;
394 | line-height: 107px;
395 | }
396 | .tile.tile-position-1-1 {
397 | -webkit-transform: translate(0px, 0px);
398 | -moz-transform: translate(0px, 0px);
399 | -ms-transform: translate(0px, 0px);
400 | transform: translate(0px, 0px);
401 | }
402 | .tile.tile-position-1-2 {
403 | -webkit-transform: translate(0px, 121px);
404 | -moz-transform: translate(0px, 121px);
405 | -ms-transform: translate(0px, 121px);
406 | transform: translate(0px, 121px);
407 | }
408 | .tile.tile-position-1-3 {
409 | -webkit-transform: translate(0px, 242px);
410 | -moz-transform: translate(0px, 242px);
411 | -ms-transform: translate(0px, 242px);
412 | transform: translate(0px, 242px);
413 | }
414 | .tile.tile-position-1-4 {
415 | -webkit-transform: translate(0px, 363px);
416 | -moz-transform: translate(0px, 363px);
417 | -ms-transform: translate(0px, 363px);
418 | transform: translate(0px, 363px);
419 | }
420 | .tile.tile-position-2-1 {
421 | -webkit-transform: translate(121px, 0px);
422 | -moz-transform: translate(121px, 0px);
423 | -ms-transform: translate(121px, 0px);
424 | transform: translate(121px, 0px);
425 | }
426 | .tile.tile-position-2-2 {
427 | -webkit-transform: translate(121px, 121px);
428 | -moz-transform: translate(121px, 121px);
429 | -ms-transform: translate(121px, 121px);
430 | transform: translate(121px, 121px);
431 | }
432 | .tile.tile-position-2-3 {
433 | -webkit-transform: translate(121px, 242px);
434 | -moz-transform: translate(121px, 242px);
435 | -ms-transform: translate(121px, 242px);
436 | transform: translate(121px, 242px);
437 | }
438 | .tile.tile-position-2-4 {
439 | -webkit-transform: translate(121px, 363px);
440 | -moz-transform: translate(121px, 363px);
441 | -ms-transform: translate(121px, 363px);
442 | transform: translate(121px, 363px);
443 | }
444 | .tile.tile-position-3-1 {
445 | -webkit-transform: translate(242px, 0px);
446 | -moz-transform: translate(242px, 0px);
447 | -ms-transform: translate(242px, 0px);
448 | transform: translate(242px, 0px);
449 | }
450 | .tile.tile-position-3-2 {
451 | -webkit-transform: translate(242px, 121px);
452 | -moz-transform: translate(242px, 121px);
453 | -ms-transform: translate(242px, 121px);
454 | transform: translate(242px, 121px);
455 | }
456 | .tile.tile-position-3-3 {
457 | -webkit-transform: translate(242px, 242px);
458 | -moz-transform: translate(242px, 242px);
459 | -ms-transform: translate(242px, 242px);
460 | transform: translate(242px, 242px);
461 | }
462 | .tile.tile-position-3-4 {
463 | -webkit-transform: translate(242px, 363px);
464 | -moz-transform: translate(242px, 363px);
465 | -ms-transform: translate(242px, 363px);
466 | transform: translate(242px, 363px);
467 | }
468 | .tile.tile-position-4-1 {
469 | -webkit-transform: translate(363px, 0px);
470 | -moz-transform: translate(363px, 0px);
471 | -ms-transform: translate(363px, 0px);
472 | transform: translate(363px, 0px);
473 | }
474 | .tile.tile-position-4-2 {
475 | -webkit-transform: translate(363px, 121px);
476 | -moz-transform: translate(363px, 121px);
477 | -ms-transform: translate(363px, 121px);
478 | transform: translate(363px, 121px);
479 | }
480 | .tile.tile-position-4-3 {
481 | -webkit-transform: translate(363px, 242px);
482 | -moz-transform: translate(363px, 242px);
483 | -ms-transform: translate(363px, 242px);
484 | transform: translate(363px, 242px);
485 | }
486 | .tile.tile-position-4-4 {
487 | -webkit-transform: translate(363px, 363px);
488 | -moz-transform: translate(363px, 363px);
489 | -ms-transform: translate(363px, 363px);
490 | transform: translate(363px, 363px);
491 | }
492 |
493 | .tile {
494 | position: absolute;
495 | -webkit-transition: 100ms ease-in-out;
496 | -moz-transition: 100ms ease-in-out;
497 | transition: 100ms ease-in-out;
498 | -webkit-transition-property: -webkit-transform;
499 | -moz-transition-property: -moz-transform;
500 | transition-property: transform;
501 | }
502 | .tile .tile-inner {
503 | border-radius: 3px;
504 | background: #eee4da;
505 | text-align: center;
506 | font-weight: bold;
507 | z-index: 10;
508 | font-size: 55px;
509 | }
510 | .tile.tile-2 .tile-inner {
511 | background: #eee4da;
512 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0),
513 | inset 0 0 0 1px rgba(255, 255, 255, 0);
514 | }
515 | .tile.tile-4 .tile-inner {
516 | background: #ede0c8;
517 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0),
518 | inset 0 0 0 1px rgba(255, 255, 255, 0);
519 | }
520 | .tile.tile-8 .tile-inner {
521 | color: #f9f6f2;
522 | background: #f2b179;
523 | }
524 | .tile.tile-16 .tile-inner {
525 | color: #f9f6f2;
526 | background: #f59563;
527 | }
528 | .tile.tile-32 .tile-inner {
529 | color: #f9f6f2;
530 | background: #f67c5f;
531 | }
532 | .tile.tile-64 .tile-inner {
533 | color: #f9f6f2;
534 | background: #f65e3b;
535 | }
536 | .tile.tile-128 .tile-inner {
537 | color: #f9f6f2;
538 | background: #edcf72;
539 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.2381),
540 | inset 0 0 0 1px rgba(255, 255, 255, 0.14286);
541 | font-size: 45px;
542 | }
543 | @media screen and (max-width: 520px) {
544 | .tile.tile-128 .tile-inner {
545 | font-size: 25px;
546 | }
547 | }
548 | .tile.tile-256 .tile-inner {
549 | color: #f9f6f2;
550 | background: #edcc61;
551 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.31746),
552 | inset 0 0 0 1px rgba(255, 255, 255, 0.19048);
553 | font-size: 45px;
554 | }
555 | @media screen and (max-width: 520px) {
556 | .tile.tile-256 .tile-inner {
557 | font-size: 25px;
558 | }
559 | }
560 | .tile.tile-512 .tile-inner {
561 | color: #f9f6f2;
562 | background: #edc850;
563 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.39683),
564 | inset 0 0 0 1px rgba(255, 255, 255, 0.2381);
565 | font-size: 45px;
566 | }
567 | @media screen and (max-width: 520px) {
568 | .tile.tile-512 .tile-inner {
569 | font-size: 25px;
570 | }
571 | }
572 | .tile.tile-1024 .tile-inner {
573 | color: #f9f6f2;
574 | background: #edc53f;
575 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.47619),
576 | inset 0 0 0 1px rgba(255, 255, 255, 0.28571);
577 | font-size: 35px;
578 | }
579 | @media screen and (max-width: 520px) {
580 | .tile.tile-1024 .tile-inner {
581 | font-size: 15px;
582 | }
583 | }
584 | .tile.tile-2048 .tile-inner {
585 | color: #f9f6f2;
586 | background: #edc22e;
587 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.55556),
588 | inset 0 0 0 1px rgba(255, 255, 255, 0.33333);
589 | font-size: 35px;
590 | }
591 | @media screen and (max-width: 520px) {
592 | .tile.tile-2048 .tile-inner {
593 | font-size: 15px;
594 | }
595 | }
596 | .tile.tile-super .tile-inner {
597 | color: #f9f6f2;
598 | background: #3c3a32;
599 | font-size: 30px;
600 | }
601 | @media screen and (max-width: 520px) {
602 | .tile.tile-super .tile-inner {
603 | font-size: 10px;
604 | }
605 | }
606 |
607 | @-webkit-keyframes appear {
608 | 0% {
609 | opacity: 0;
610 | -webkit-transform: scale(0);
611 | -moz-transform: scale(0);
612 | -ms-transform: scale(0);
613 | transform: scale(0);
614 | }
615 |
616 | 100% {
617 | opacity: 1;
618 | -webkit-transform: scale(1);
619 | -moz-transform: scale(1);
620 | -ms-transform: scale(1);
621 | transform: scale(1);
622 | }
623 | }
624 | @-moz-keyframes appear {
625 | 0% {
626 | opacity: 0;
627 | -webkit-transform: scale(0);
628 | -moz-transform: scale(0);
629 | -ms-transform: scale(0);
630 | transform: scale(0);
631 | }
632 |
633 | 100% {
634 | opacity: 1;
635 | -webkit-transform: scale(1);
636 | -moz-transform: scale(1);
637 | -ms-transform: scale(1);
638 | transform: scale(1);
639 | }
640 | }
641 | @keyframes appear {
642 | 0% {
643 | opacity: 0;
644 | -webkit-transform: scale(0);
645 | -moz-transform: scale(0);
646 | -ms-transform: scale(0);
647 | transform: scale(0);
648 | }
649 |
650 | 100% {
651 | opacity: 1;
652 | -webkit-transform: scale(1);
653 | -moz-transform: scale(1);
654 | -ms-transform: scale(1);
655 | transform: scale(1);
656 | }
657 | }
658 | .tile-new .tile-inner {
659 | -webkit-animation: appear 200ms ease 100ms;
660 | -moz-animation: appear 200ms ease 100ms;
661 | animation: appear 200ms ease 100ms;
662 | -webkit-animation-fill-mode: backwards;
663 | -moz-animation-fill-mode: backwards;
664 | animation-fill-mode: backwards;
665 | }
666 |
667 | @-webkit-keyframes pop {
668 | 0% {
669 | -webkit-transform: scale(0);
670 | -moz-transform: scale(0);
671 | -ms-transform: scale(0);
672 | transform: scale(0);
673 | }
674 |
675 | 50% {
676 | -webkit-transform: scale(1.2);
677 | -moz-transform: scale(1.2);
678 | -ms-transform: scale(1.2);
679 | transform: scale(1.2);
680 | }
681 |
682 | 100% {
683 | -webkit-transform: scale(1);
684 | -moz-transform: scale(1);
685 | -ms-transform: scale(1);
686 | transform: scale(1);
687 | }
688 | }
689 | @-moz-keyframes pop {
690 | 0% {
691 | -webkit-transform: scale(0);
692 | -moz-transform: scale(0);
693 | -ms-transform: scale(0);
694 | transform: scale(0);
695 | }
696 |
697 | 50% {
698 | -webkit-transform: scale(1.2);
699 | -moz-transform: scale(1.2);
700 | -ms-transform: scale(1.2);
701 | transform: scale(1.2);
702 | }
703 |
704 | 100% {
705 | -webkit-transform: scale(1);
706 | -moz-transform: scale(1);
707 | -ms-transform: scale(1);
708 | transform: scale(1);
709 | }
710 | }
711 | @keyframes pop {
712 | 0% {
713 | -webkit-transform: scale(0);
714 | -moz-transform: scale(0);
715 | -ms-transform: scale(0);
716 | transform: scale(0);
717 | }
718 |
719 | 50% {
720 | -webkit-transform: scale(1.2);
721 | -moz-transform: scale(1.2);
722 | -ms-transform: scale(1.2);
723 | transform: scale(1.2);
724 | }
725 |
726 | 100% {
727 | -webkit-transform: scale(1);
728 | -moz-transform: scale(1);
729 | -ms-transform: scale(1);
730 | transform: scale(1);
731 | }
732 | }
733 | .tile-merged .tile-inner {
734 | z-index: 20;
735 | -webkit-animation: pop 200ms ease 100ms;
736 | -moz-animation: pop 200ms ease 100ms;
737 | animation: pop 200ms ease 100ms;
738 | -webkit-animation-fill-mode: backwards;
739 | -moz-animation-fill-mode: backwards;
740 | animation-fill-mode: backwards;
741 | }
742 |
743 | .above-game:after {
744 | content: '';
745 | display: block;
746 | clear: both;
747 | }
748 |
749 | .game-intro {
750 | float: left;
751 | line-height: 42px;
752 | margin-bottom: 0;
753 | }
754 |
755 | .restart-button {
756 | display: inline-block;
757 | background: #8f7a66;
758 | border-radius: 3px;
759 | padding: 0 20px;
760 | text-decoration: none;
761 | color: #f9f6f2;
762 | height: 40px;
763 | line-height: 42px;
764 | cursor: pointer;
765 | display: block;
766 | text-align: center;
767 | float: right;
768 | }
769 |
770 | .game-explanation {
771 | margin-top: 30px;
772 | }
773 |
774 | .sharing {
775 | margin-top: 20px;
776 | text-align: center;
777 | }
778 | .sharing > iframe,
779 | .sharing > span,
780 | .sharing > form {
781 | display: inline-block;
782 | vertical-align: middle;
783 | }
784 |
785 | @media screen and (max-width: 520px) {
786 | html,
787 | body {
788 | font-size: 15px;
789 | }
790 |
791 | body {
792 | margin-top: 0;
793 | padding: 20px;
794 | }
795 |
796 | h1.title {
797 | font-size: 27px;
798 | margin-top: 15px;
799 | }
800 |
801 | .container {
802 | width: 280px;
803 | margin: 0 auto;
804 | }
805 |
806 | .score-container,
807 | .best-container {
808 | margin-top: 0;
809 | padding: 15px 10px;
810 | min-width: 40px;
811 | }
812 |
813 | .heading {
814 | margin-bottom: 10px;
815 | }
816 |
817 | .game-intro {
818 | width: 55%;
819 | display: block;
820 | box-sizing: border-box;
821 | line-height: 1.65;
822 | }
823 |
824 | .restart-button {
825 | width: 42%;
826 | padding: 0;
827 | display: block;
828 | box-sizing: border-box;
829 | margin-top: 2px;
830 | }
831 |
832 | .game-container {
833 | margin-top: 17px;
834 | position: relative;
835 | padding: 10px;
836 | cursor: default;
837 | -webkit-touch-callout: none;
838 | -ms-touch-callout: none;
839 | -webkit-user-select: none;
840 | -moz-user-select: none;
841 | -ms-user-select: none;
842 | -ms-touch-action: none;
843 | touch-action: none;
844 | background: #bbada0;
845 | border-radius: 6px;
846 | width: 280px;
847 | height: 280px;
848 | -webkit-box-sizing: border-box;
849 | -moz-box-sizing: border-box;
850 | box-sizing: border-box;
851 | }
852 |
853 | .game-message {
854 | display: none;
855 | position: absolute;
856 | top: 0;
857 | right: 0;
858 | bottom: 0;
859 | left: 0;
860 | background: rgba(238, 228, 218, 0.73);
861 | z-index: 100;
862 | padding-top: 40px;
863 | text-align: center;
864 | -webkit-animation: fade-in 800ms ease 1200ms;
865 | -moz-animation: fade-in 800ms ease 1200ms;
866 | animation: fade-in 800ms ease 1200ms;
867 | -webkit-animation-fill-mode: both;
868 | -moz-animation-fill-mode: both;
869 | animation-fill-mode: both;
870 | }
871 | .game-message p {
872 | font-size: 60px;
873 | font-weight: bold;
874 | height: 60px;
875 | line-height: 60px;
876 | margin-top: 222px;
877 | }
878 | .game-message .lower {
879 | display: block;
880 | margin-top: 29px;
881 | }
882 | .game-message .mailing-list {
883 | margin-top: 52px;
884 | }
885 | .game-message .mailing-list strong {
886 | display: block;
887 | margin-bottom: 10px;
888 | }
889 | .game-message .mailing-list .mailing-list-email-field {
890 | width: 230px;
891 | margin-right: 5px;
892 | }
893 | .game-message a {
894 | display: inline-block;
895 | background: #8f7a66;
896 | border-radius: 3px;
897 | padding: 0 20px;
898 | text-decoration: none;
899 | color: #f9f6f2;
900 | height: 40px;
901 | line-height: 42px;
902 | cursor: pointer;
903 | margin-left: 9px;
904 | }
905 | .game-message a.keep-playing-button {
906 | display: none;
907 | }
908 | .game-message .score-sharing {
909 | display: inline-block;
910 | vertical-align: middle;
911 | margin-left: 10px;
912 | }
913 | .game-message.game-won {
914 | background: rgba(237, 194, 46, 0.5);
915 | color: #f9f6f2;
916 | }
917 | .game-message.game-won a.keep-playing-button {
918 | display: inline-block;
919 | }
920 | .game-message.game-won,
921 | .game-message.game-over {
922 | display: block;
923 | }
924 | .game-message.game-won p,
925 | .game-message.game-over p {
926 | -webkit-animation: slide-up 1.5s ease-in-out 2500ms;
927 | -moz-animation: slide-up 1.5s ease-in-out 2500ms;
928 | animation: slide-up 1.5s ease-in-out 2500ms;
929 | -webkit-animation-fill-mode: both;
930 | -moz-animation-fill-mode: both;
931 | animation-fill-mode: both;
932 | }
933 | .game-message.game-won .mailing-list,
934 | .game-message.game-over .mailing-list {
935 | -webkit-animation: fade-in 1.5s ease-in-out 2500ms;
936 | -moz-animation: fade-in 1.5s ease-in-out 2500ms;
937 | animation: fade-in 1.5s ease-in-out 2500ms;
938 | -webkit-animation-fill-mode: both;
939 | -moz-animation-fill-mode: both;
940 | animation-fill-mode: both;
941 | }
942 |
943 | .grid-container {
944 | position: absolute;
945 | z-index: 1;
946 | }
947 |
948 | .grid-row {
949 | margin-bottom: 10px;
950 | }
951 | .grid-row:last-child {
952 | margin-bottom: 0;
953 | }
954 | .grid-row:after {
955 | content: '';
956 | display: block;
957 | clear: both;
958 | }
959 |
960 | .grid-cell {
961 | width: 57.5px;
962 | height: 57.5px;
963 | margin-right: 10px;
964 | float: left;
965 | border-radius: 3px;
966 | background: rgba(238, 228, 218, 0.35);
967 | }
968 | .grid-cell:last-child {
969 | margin-right: 0;
970 | }
971 |
972 | .tile-container {
973 | position: absolute;
974 | z-index: 2;
975 | }
976 |
977 | .tile,
978 | .tile .tile-inner {
979 | width: 58px;
980 | height: 58px;
981 | line-height: 58px;
982 | }
983 | .tile.tile-position-1-1 {
984 | -webkit-transform: translate(0px, 0px);
985 | -moz-transform: translate(0px, 0px);
986 | -ms-transform: translate(0px, 0px);
987 | transform: translate(0px, 0px);
988 | }
989 | .tile.tile-position-1-2 {
990 | -webkit-transform: translate(0px, 67px);
991 | -moz-transform: translate(0px, 67px);
992 | -ms-transform: translate(0px, 67px);
993 | transform: translate(0px, 67px);
994 | }
995 | .tile.tile-position-1-3 {
996 | -webkit-transform: translate(0px, 135px);
997 | -moz-transform: translate(0px, 135px);
998 | -ms-transform: translate(0px, 135px);
999 | transform: translate(0px, 135px);
1000 | }
1001 | .tile.tile-position-1-4 {
1002 | -webkit-transform: translate(0px, 202px);
1003 | -moz-transform: translate(0px, 202px);
1004 | -ms-transform: translate(0px, 202px);
1005 | transform: translate(0px, 202px);
1006 | }
1007 | .tile.tile-position-2-1 {
1008 | -webkit-transform: translate(67px, 0px);
1009 | -moz-transform: translate(67px, 0px);
1010 | -ms-transform: translate(67px, 0px);
1011 | transform: translate(67px, 0px);
1012 | }
1013 | .tile.tile-position-2-2 {
1014 | -webkit-transform: translate(67px, 67px);
1015 | -moz-transform: translate(67px, 67px);
1016 | -ms-transform: translate(67px, 67px);
1017 | transform: translate(67px, 67px);
1018 | }
1019 | .tile.tile-position-2-3 {
1020 | -webkit-transform: translate(67px, 135px);
1021 | -moz-transform: translate(67px, 135px);
1022 | -ms-transform: translate(67px, 135px);
1023 | transform: translate(67px, 135px);
1024 | }
1025 | .tile.tile-position-2-4 {
1026 | -webkit-transform: translate(67px, 202px);
1027 | -moz-transform: translate(67px, 202px);
1028 | -ms-transform: translate(67px, 202px);
1029 | transform: translate(67px, 202px);
1030 | }
1031 | .tile.tile-position-3-1 {
1032 | -webkit-transform: translate(135px, 0px);
1033 | -moz-transform: translate(135px, 0px);
1034 | -ms-transform: translate(135px, 0px);
1035 | transform: translate(135px, 0px);
1036 | }
1037 | .tile.tile-position-3-2 {
1038 | -webkit-transform: translate(135px, 67px);
1039 | -moz-transform: translate(135px, 67px);
1040 | -ms-transform: translate(135px, 67px);
1041 | transform: translate(135px, 67px);
1042 | }
1043 | .tile.tile-position-3-3 {
1044 | -webkit-transform: translate(135px, 135px);
1045 | -moz-transform: translate(135px, 135px);
1046 | -ms-transform: translate(135px, 135px);
1047 | transform: translate(135px, 135px);
1048 | }
1049 | .tile.tile-position-3-4 {
1050 | -webkit-transform: translate(135px, 202px);
1051 | -moz-transform: translate(135px, 202px);
1052 | -ms-transform: translate(135px, 202px);
1053 | transform: translate(135px, 202px);
1054 | }
1055 | .tile.tile-position-4-1 {
1056 | -webkit-transform: translate(202px, 0px);
1057 | -moz-transform: translate(202px, 0px);
1058 | -ms-transform: translate(202px, 0px);
1059 | transform: translate(202px, 0px);
1060 | }
1061 | .tile.tile-position-4-2 {
1062 | -webkit-transform: translate(202px, 67px);
1063 | -moz-transform: translate(202px, 67px);
1064 | -ms-transform: translate(202px, 67px);
1065 | transform: translate(202px, 67px);
1066 | }
1067 | .tile.tile-position-4-3 {
1068 | -webkit-transform: translate(202px, 135px);
1069 | -moz-transform: translate(202px, 135px);
1070 | -ms-transform: translate(202px, 135px);
1071 | transform: translate(202px, 135px);
1072 | }
1073 | .tile.tile-position-4-4 {
1074 | -webkit-transform: translate(202px, 202px);
1075 | -moz-transform: translate(202px, 202px);
1076 | -ms-transform: translate(202px, 202px);
1077 | transform: translate(202px, 202px);
1078 | }
1079 |
1080 | .tile .tile-inner {
1081 | font-size: 35px;
1082 | }
1083 |
1084 | .game-message {
1085 | padding-top: 0;
1086 | }
1087 | .game-message p {
1088 | font-size: 30px !important;
1089 | height: 30px !important;
1090 | line-height: 30px !important;
1091 | margin-top: 32% !important;
1092 | margin-bottom: 0 !important;
1093 | }
1094 | .game-message .lower {
1095 | margin-top: 10px !important;
1096 | }
1097 | .game-message.game-won .score-sharing {
1098 | margin-top: 10px;
1099 | }
1100 | .game-message.game-over .mailing-list {
1101 | margin-top: 25px;
1102 | }
1103 | .game-message .mailing-list {
1104 | margin-top: 10px;
1105 | }
1106 | .game-message .mailing-list .mailing-list-email-field {
1107 | width: 180px;
1108 | }
1109 |
1110 | .sharing > iframe,
1111 | .sharing > span,
1112 | .sharing > form {
1113 | display: block;
1114 | margin: 0 auto;
1115 | margin-bottom: 20px;
1116 | }
1117 | }
1118 | .pp-donate button {
1119 | -webkit-appearance: none;
1120 | -moz-appearance: none;
1121 | appearance: none;
1122 | border: none;
1123 | font: inherit;
1124 | color: inherit;
1125 | display: inline-block;
1126 | background: #8f7a66;
1127 | border-radius: 3px;
1128 | padding: 0 20px;
1129 | text-decoration: none;
1130 | color: #f9f6f2;
1131 | height: 40px;
1132 | line-height: 42px;
1133 | cursor: pointer;
1134 | }
1135 | .pp-donate button img {
1136 | vertical-align: -4px;
1137 | margin-right: 8px;
1138 | }
1139 |
1140 | .btc-donate {
1141 | position: relative;
1142 | margin-left: 10px;
1143 | display: inline-block;
1144 | background: #8f7a66;
1145 | border-radius: 3px;
1146 | padding: 0 20px;
1147 | text-decoration: none;
1148 | color: #f9f6f2;
1149 | height: 40px;
1150 | line-height: 42px;
1151 | cursor: pointer;
1152 | }
1153 | .btc-donate img {
1154 | vertical-align: -4px;
1155 | margin-right: 8px;
1156 | }
1157 | .btc-donate a {
1158 | color: #f9f6f2;
1159 | text-decoration: none;
1160 | font-weight: normal;
1161 | }
1162 | .btc-donate .address {
1163 | cursor: auto;
1164 | position: absolute;
1165 | width: 340px;
1166 | right: 50%;
1167 | margin-right: -170px;
1168 | padding-bottom: 7px;
1169 | top: -30px;
1170 | opacity: 0;
1171 | pointer-events: none;
1172 | -webkit-transition: 400ms ease;
1173 | -moz-transition: 400ms ease;
1174 | transition: 400ms ease;
1175 | -webkit-transition-property: top, opacity;
1176 | -moz-transition-property: top, opacity;
1177 | transition-property: top, opacity;
1178 | }
1179 | .btc-donate .address:after {
1180 | position: absolute;
1181 | border-top: 10px solid #bbada0;
1182 | border-right: 7px solid transparent;
1183 | border-left: 7px solid transparent;
1184 | content: '';
1185 | bottom: 0px;
1186 | left: 50%;
1187 | margin-left: -7px;
1188 | }
1189 | .btc-donate .address code {
1190 | background-color: #bbada0;
1191 | padding: 10px 15px;
1192 | width: 100%;
1193 | border-radius: 3px;
1194 | line-height: 1;
1195 | font-weight: normal;
1196 | font-size: 15px;
1197 | font-family: Consolas, 'Liberation Mono', Courier, monospace;
1198 | text-align: center;
1199 | }
1200 | .btc-donate:hover .address,
1201 | .btc-donate .address:hover .address {
1202 | opacity: 1;
1203 | top: -45px;
1204 | pointer-events: auto;
1205 | }
1206 | @media screen and (max-width: 520px) {
1207 | .btc-donate {
1208 | width: 120px;
1209 | }
1210 | .btc-donate .address {
1211 | margin-right: -150px;
1212 | width: 300px;
1213 | }
1214 | .btc-donate .address code {
1215 | font-size: 13px;
1216 | }
1217 | .btc-donate .address:after {
1218 | left: 50%;
1219 | bottom: 2px;
1220 | }
1221 | }
1222 |
1223 | @-webkit-keyframes pop-in-big {
1224 | 0% {
1225 | -webkit-transform: scale(0) translateZ(0);
1226 | -moz-transform: scale(0) translateZ(0);
1227 | transform: scale(0) translateZ(0);
1228 | opacity: 0;
1229 | margin-top: -40px;
1230 | }
1231 |
1232 | 100% {
1233 | -webkit-transform: scale(1) translateZ(0);
1234 | -moz-transform: scale(1) translateZ(0);
1235 | transform: scale(1) translateZ(0);
1236 | opacity: 1;
1237 | margin-top: 30px;
1238 | }
1239 | }
1240 | @-moz-keyframes pop-in-big {
1241 | 0% {
1242 | -webkit-transform: scale(0) translateZ(0);
1243 | -moz-transform: scale(0) translateZ(0);
1244 | transform: scale(0) translateZ(0);
1245 | opacity: 0;
1246 | margin-top: -40px;
1247 | }
1248 |
1249 | 100% {
1250 | -webkit-transform: scale(1) translateZ(0);
1251 | -moz-transform: scale(1) translateZ(0);
1252 | transform: scale(1) translateZ(0);
1253 | opacity: 1;
1254 | margin-top: 30px;
1255 | }
1256 | }
1257 | @keyframes pop-in-big {
1258 | 0% {
1259 | -webkit-transform: scale(0) translateZ(0);
1260 | -moz-transform: scale(0) translateZ(0);
1261 | transform: scale(0) translateZ(0);
1262 | opacity: 0;
1263 | margin-top: -40px;
1264 | }
1265 |
1266 | 100% {
1267 | -webkit-transform: scale(1) translateZ(0);
1268 | -moz-transform: scale(1) translateZ(0);
1269 | transform: scale(1) translateZ(0);
1270 | opacity: 1;
1271 | margin-top: 30px;
1272 | }
1273 | }
1274 | @-webkit-keyframes pop-in-small {
1275 | 0% {
1276 | -webkit-transform: scale(0) translateZ(0);
1277 | -moz-transform: scale(0) translateZ(0);
1278 | transform: scale(0) translateZ(0);
1279 | opacity: 0;
1280 | margin-top: -40px;
1281 | }
1282 |
1283 | 100% {
1284 | -webkit-transform: scale(1) translateZ(0);
1285 | -moz-transform: scale(1) translateZ(0);
1286 | transform: scale(1) translateZ(0);
1287 | opacity: 1;
1288 | margin-top: 10px;
1289 | }
1290 | }
1291 | @-moz-keyframes pop-in-small {
1292 | 0% {
1293 | -webkit-transform: scale(0) translateZ(0);
1294 | -moz-transform: scale(0) translateZ(0);
1295 | transform: scale(0) translateZ(0);
1296 | opacity: 0;
1297 | margin-top: -40px;
1298 | }
1299 |
1300 | 100% {
1301 | -webkit-transform: scale(1) translateZ(0);
1302 | -moz-transform: scale(1) translateZ(0);
1303 | transform: scale(1) translateZ(0);
1304 | opacity: 1;
1305 | margin-top: 10px;
1306 | }
1307 | }
1308 | @keyframes pop-in-small {
1309 | 0% {
1310 | -webkit-transform: scale(0) translateZ(0);
1311 | -moz-transform: scale(0) translateZ(0);
1312 | transform: scale(0) translateZ(0);
1313 | opacity: 0;
1314 | margin-top: -40px;
1315 | }
1316 |
1317 | 100% {
1318 | -webkit-transform: scale(1) translateZ(0);
1319 | -moz-transform: scale(1) translateZ(0);
1320 | transform: scale(1) translateZ(0);
1321 | opacity: 1;
1322 | margin-top: 10px;
1323 | }
1324 | }
1325 | .app-notice {
1326 | position: relative;
1327 | -webkit-animation: pop-in-big 700ms ease 2s both;
1328 | -moz-animation: pop-in-big 700ms ease 2s both;
1329 | animation: pop-in-big 700ms ease 2s both;
1330 | background: #edc53f;
1331 | color: white;
1332 | padding: 10px;
1333 | margin-top: 30px;
1334 | height: 40px;
1335 | box-sizing: border-box;
1336 | border-radius: 3px;
1337 | }
1338 | .app-notice:after {
1339 | content: '';
1340 | display: block;
1341 | clear: both;
1342 | }
1343 | .app-notice .notice-close-button {
1344 | float: right;
1345 | font-weight: bold;
1346 | cursor: pointer;
1347 | margin-left: 10px;
1348 | opacity: 0.7;
1349 | }
1350 | .app-notice p {
1351 | margin-bottom: 0;
1352 | }
1353 | .app-notice,
1354 | .app-notice p {
1355 | line-height: 20px;
1356 | }
1357 | .app-notice a {
1358 | color: white;
1359 | }
1360 | @media screen and (max-width: 520px) {
1361 | .app-notice {
1362 | -webkit-animation: pop-in-small 700ms ease 2s both;
1363 | -moz-animation: pop-in-small 700ms ease 2s both;
1364 | animation: pop-in-small 700ms ease 2s both;
1365 | margin-top: 10px;
1366 | height: 40px;
1367 | }
1368 | }
1369 |
1370 | .links {
1371 | text-align: center;
1372 | margin-top: 20px;
1373 | }
1374 |
1375 | .privacy {
1376 | word-wrap: break-word;
1377 | }
1378 |
1379 | /* extras */
1380 | .sidebar {
1381 | width: 180px;
1382 | top: 0;
1383 | bottom: 0;
1384 | right: 0;
1385 | position: fixed;
1386 | display: flex;
1387 | align-items: center;
1388 | justify-content: center;
1389 | }
1390 |
1391 | @media (max-width: 880px) {
1392 | .sidebar,
1393 | .sidebar .adsbygoogle {
1394 | display: none;
1395 | }
1396 | }
1397 |
1398 | .under-board-container {
1399 | margin-top: 38px;
1400 | }
1401 |
1402 | .under-board-container,
1403 | .under-board-container .adsbygoogle {
1404 | width: 100%;
1405 | height: 80px;
1406 | display: none;
1407 | }
1408 |
1409 | @media (max-width: 880px) {
1410 | .under-board-container,
1411 | .under-board-container .adsbygoogle {
1412 | display: block;
1413 | }
1414 | }
1415 |
1416 | .cookie-notice {
1417 | position: fixed;
1418 | font-size: 15px;
1419 | z-index: 999;
1420 | right: 20px;
1421 | bottom: 20px;
1422 | width: 20%;
1423 | min-width: 460px;
1424 | background: #e8e5db;
1425 | padding: 10px;
1426 | margin-top: 30px;
1427 | box-sizing: border-box;
1428 | border-radius: 3px;
1429 | display: flex;
1430 | align-items: center;
1431 | justify-content: center;
1432 | }
1433 | @media screen and (max-width: 520px) {
1434 | .cookie-notice {
1435 | width: auto;
1436 | left: 20px;
1437 | min-width: auto;
1438 | }
1439 | }
1440 | .cookie-notice,
1441 | .cookie-notice p a {
1442 | color: #a09488;
1443 | }
1444 | .cookie-notice p {
1445 | margin-bottom: 0;
1446 | flex: 1;
1447 | }
1448 | .cookie-notice,
1449 | .cookie-notice p {
1450 | line-height: 20px;
1451 | }
1452 |
1453 | .cookie-notice-dismiss-button {
1454 | display: inline-block;
1455 | background: #8f7a66;
1456 | border-radius: 3px;
1457 | padding: 0 20px;
1458 | text-decoration: none;
1459 | color: #f9f6f2;
1460 | height: 40px;
1461 | line-height: 42px;
1462 | cursor: pointer;
1463 | flex: 0 0 auto;
1464 | margin-left: 20px;
1465 | }
1466 |
--------------------------------------------------------------------------------
/game2048/final/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import './index.css';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
--------------------------------------------------------------------------------
/game2048/final/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/game2048/final/src/util/__snapshots__/tile.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`moveTile 가로 2 4 8 8 x=-1 1`] = `
4 | Array [
5 | Object {
6 | "value": 2,
7 | "x": 1,
8 | "y": 1,
9 | },
10 | Object {
11 | "value": 4,
12 | "x": 2,
13 | "y": 1,
14 | },
15 | Object {
16 | "isDisabled": true,
17 | "value": 8,
18 | "x": 3,
19 | "y": 1,
20 | },
21 | Object {
22 | "isDisabled": true,
23 | "value": 8,
24 | "x": 3,
25 | "y": 1,
26 | },
27 | Object {
28 | "isMerged": true,
29 | "value": 16,
30 | "x": 3,
31 | "y": 1,
32 | },
33 | ]
34 | `;
35 |
36 | exports[`moveTile 가로 2 4 8 8 x=1 1`] = `
37 | Array [
38 | Object {
39 | "isDisabled": true,
40 | "value": 8,
41 | "x": 4,
42 | "y": 1,
43 | },
44 | Object {
45 | "isDisabled": true,
46 | "value": 8,
47 | "x": 4,
48 | "y": 1,
49 | },
50 | Object {
51 | "value": 4,
52 | "x": 3,
53 | "y": 1,
54 | },
55 | Object {
56 | "value": 2,
57 | "x": 2,
58 | "y": 1,
59 | },
60 | Object {
61 | "isMerged": true,
62 | "value": 16,
63 | "x": 4,
64 | "y": 1,
65 | },
66 | ]
67 | `;
68 |
69 | exports[`moveTile 가로 2 x 8 8 x=-1 1`] = `
70 | Array [
71 | Object {
72 | "value": 2,
73 | "x": 1,
74 | "y": 1,
75 | },
76 | Object {
77 | "isDisabled": true,
78 | "value": 8,
79 | "x": 2,
80 | "y": 1,
81 | },
82 | Object {
83 | "isDisabled": true,
84 | "value": 8,
85 | "x": 2,
86 | "y": 1,
87 | },
88 | Object {
89 | "isMerged": true,
90 | "value": 16,
91 | "x": 2,
92 | "y": 1,
93 | },
94 | ]
95 | `;
96 |
97 | exports[`moveTile 가로 2 x 8 8 x=1 1`] = `
98 | Array [
99 | Object {
100 | "isDisabled": true,
101 | "value": 8,
102 | "x": 4,
103 | "y": 1,
104 | },
105 | Object {
106 | "isDisabled": true,
107 | "value": 8,
108 | "x": 4,
109 | "y": 1,
110 | },
111 | Object {
112 | "value": 2,
113 | "x": 3,
114 | "y": 1,
115 | },
116 | Object {
117 | "isMerged": true,
118 | "value": 16,
119 | "x": 4,
120 | "y": 1,
121 | },
122 | ]
123 | `;
124 |
125 | exports[`moveTile 가로 8 8 4 4 x=-1 1`] = `
126 | Array [
127 | Object {
128 | "isDisabled": true,
129 | "value": 8,
130 | "x": 1,
131 | "y": 1,
132 | },
133 | Object {
134 | "isDisabled": true,
135 | "value": 8,
136 | "x": 1,
137 | "y": 1,
138 | },
139 | Object {
140 | "isDisabled": true,
141 | "value": 4,
142 | "x": 2,
143 | "y": 1,
144 | },
145 | Object {
146 | "isDisabled": true,
147 | "value": 4,
148 | "x": 2,
149 | "y": 1,
150 | },
151 | Object {
152 | "isMerged": true,
153 | "value": 16,
154 | "x": 1,
155 | "y": 1,
156 | },
157 | Object {
158 | "isMerged": true,
159 | "value": 8,
160 | "x": 2,
161 | "y": 1,
162 | },
163 | ]
164 | `;
165 |
166 | exports[`moveTile 가로 8 8 4 4 x=1 1`] = `
167 | Array [
168 | Object {
169 | "isDisabled": true,
170 | "value": 4,
171 | "x": 4,
172 | "y": 1,
173 | },
174 | Object {
175 | "isDisabled": true,
176 | "value": 4,
177 | "x": 4,
178 | "y": 1,
179 | },
180 | Object {
181 | "isDisabled": true,
182 | "value": 8,
183 | "x": 3,
184 | "y": 1,
185 | },
186 | Object {
187 | "isDisabled": true,
188 | "value": 8,
189 | "x": 3,
190 | "y": 1,
191 | },
192 | Object {
193 | "isMerged": true,
194 | "value": 8,
195 | "x": 4,
196 | "y": 1,
197 | },
198 | Object {
199 | "isMerged": true,
200 | "value": 16,
201 | "x": 3,
202 | "y": 1,
203 | },
204 | ]
205 | `;
206 |
207 | exports[`moveTile 가로 8 8 4 4 y=-1 1`] = `
208 | Array [
209 | Object {
210 | "value": 8,
211 | "x": 1,
212 | "y": 1,
213 | },
214 | Object {
215 | "value": 8,
216 | "x": 2,
217 | "y": 1,
218 | },
219 | Object {
220 | "value": 4,
221 | "x": 3,
222 | "y": 1,
223 | },
224 | Object {
225 | "value": 4,
226 | "x": 4,
227 | "y": 1,
228 | },
229 | ]
230 | `;
231 |
232 | exports[`moveTile 가로 8 8 4 4 y=1 1`] = `
233 | Array [
234 | Object {
235 | "value": 8,
236 | "x": 1,
237 | "y": 4,
238 | },
239 | Object {
240 | "value": 8,
241 | "x": 2,
242 | "y": 4,
243 | },
244 | Object {
245 | "value": 4,
246 | "x": 3,
247 | "y": 4,
248 | },
249 | Object {
250 | "value": 4,
251 | "x": 4,
252 | "y": 4,
253 | },
254 | ]
255 | `;
256 |
257 | exports[`moveTile 가로 여러 줄, 2 2 4 4, 8 x 4 4 x=-1 1`] = `
258 | Array [
259 | Object {
260 | "isDisabled": true,
261 | "value": 2,
262 | "x": 1,
263 | "y": 1,
264 | },
265 | Object {
266 | "isDisabled": true,
267 | "value": 2,
268 | "x": 1,
269 | "y": 1,
270 | },
271 | Object {
272 | "isDisabled": true,
273 | "value": 4,
274 | "x": 2,
275 | "y": 1,
276 | },
277 | Object {
278 | "isDisabled": true,
279 | "value": 4,
280 | "x": 2,
281 | "y": 1,
282 | },
283 | Object {
284 | "value": 8,
285 | "x": 1,
286 | "y": 2,
287 | },
288 | Object {
289 | "isDisabled": true,
290 | "value": 4,
291 | "x": 2,
292 | "y": 2,
293 | },
294 | Object {
295 | "isDisabled": true,
296 | "value": 4,
297 | "x": 2,
298 | "y": 2,
299 | },
300 | Object {
301 | "isMerged": true,
302 | "value": 4,
303 | "x": 1,
304 | "y": 1,
305 | },
306 | Object {
307 | "isMerged": true,
308 | "value": 8,
309 | "x": 2,
310 | "y": 1,
311 | },
312 | Object {
313 | "isMerged": true,
314 | "value": 8,
315 | "x": 2,
316 | "y": 2,
317 | },
318 | ]
319 | `;
320 |
321 | exports[`moveTile 가로 여러 줄, 2 2 4 4, 8 x 4 4 x=1 1`] = `
322 | Array [
323 | Object {
324 | "isDisabled": true,
325 | "value": 4,
326 | "x": 4,
327 | "y": 1,
328 | },
329 | Object {
330 | "isDisabled": true,
331 | "value": 4,
332 | "x": 4,
333 | "y": 1,
334 | },
335 | Object {
336 | "isDisabled": true,
337 | "value": 2,
338 | "x": 3,
339 | "y": 1,
340 | },
341 | Object {
342 | "isDisabled": true,
343 | "value": 2,
344 | "x": 3,
345 | "y": 1,
346 | },
347 | Object {
348 | "isDisabled": true,
349 | "value": 4,
350 | "x": 4,
351 | "y": 2,
352 | },
353 | Object {
354 | "isDisabled": true,
355 | "value": 4,
356 | "x": 4,
357 | "y": 2,
358 | },
359 | Object {
360 | "value": 8,
361 | "x": 3,
362 | "y": 2,
363 | },
364 | Object {
365 | "isMerged": true,
366 | "value": 8,
367 | "x": 4,
368 | "y": 1,
369 | },
370 | Object {
371 | "isMerged": true,
372 | "value": 4,
373 | "x": 3,
374 | "y": 1,
375 | },
376 | Object {
377 | "isMerged": true,
378 | "value": 8,
379 | "x": 4,
380 | "y": 2,
381 | },
382 | ]
383 | `;
384 |
385 | exports[`moveTile 세로 2 x 8 8 y=-1 1`] = `
386 | Array [
387 | Object {
388 | "value": 2,
389 | "x": 1,
390 | "y": 1,
391 | },
392 | Object {
393 | "isDisabled": true,
394 | "value": 8,
395 | "x": 1,
396 | "y": 2,
397 | },
398 | Object {
399 | "isDisabled": true,
400 | "value": 8,
401 | "x": 1,
402 | "y": 2,
403 | },
404 | Object {
405 | "isMerged": true,
406 | "value": 16,
407 | "x": 1,
408 | "y": 2,
409 | },
410 | ]
411 | `;
412 |
413 | exports[`moveTile 세로 2 x 8 8 y=1 1`] = `
414 | Array [
415 | Object {
416 | "isDisabled": true,
417 | "value": 8,
418 | "x": 1,
419 | "y": 4,
420 | },
421 | Object {
422 | "isDisabled": true,
423 | "value": 8,
424 | "x": 1,
425 | "y": 4,
426 | },
427 | Object {
428 | "value": 2,
429 | "x": 1,
430 | "y": 3,
431 | },
432 | Object {
433 | "isMerged": true,
434 | "value": 16,
435 | "x": 1,
436 | "y": 4,
437 | },
438 | ]
439 | `;
440 |
441 | exports[`moveTile 세로 8 8 4 4 x=-1 1`] = `
442 | Array [
443 | Object {
444 | "value": 8,
445 | "x": 1,
446 | "y": 1,
447 | },
448 | Object {
449 | "value": 8,
450 | "x": 1,
451 | "y": 2,
452 | },
453 | Object {
454 | "value": 4,
455 | "x": 1,
456 | "y": 3,
457 | },
458 | Object {
459 | "value": 4,
460 | "x": 1,
461 | "y": 4,
462 | },
463 | ]
464 | `;
465 |
466 | exports[`moveTile 세로 8 8 4 4 x=1 1`] = `
467 | Array [
468 | Object {
469 | "value": 8,
470 | "x": 4,
471 | "y": 1,
472 | },
473 | Object {
474 | "value": 8,
475 | "x": 4,
476 | "y": 2,
477 | },
478 | Object {
479 | "value": 4,
480 | "x": 4,
481 | "y": 3,
482 | },
483 | Object {
484 | "value": 4,
485 | "x": 4,
486 | "y": 4,
487 | },
488 | ]
489 | `;
490 |
491 | exports[`moveTile 세로 8 8 4 4 y=-1 1`] = `
492 | Array [
493 | Object {
494 | "isDisabled": true,
495 | "value": 8,
496 | "x": 1,
497 | "y": 1,
498 | },
499 | Object {
500 | "isDisabled": true,
501 | "value": 8,
502 | "x": 1,
503 | "y": 1,
504 | },
505 | Object {
506 | "isDisabled": true,
507 | "value": 4,
508 | "x": 1,
509 | "y": 2,
510 | },
511 | Object {
512 | "isDisabled": true,
513 | "value": 4,
514 | "x": 1,
515 | "y": 2,
516 | },
517 | Object {
518 | "isMerged": true,
519 | "value": 16,
520 | "x": 1,
521 | "y": 1,
522 | },
523 | Object {
524 | "isMerged": true,
525 | "value": 8,
526 | "x": 1,
527 | "y": 2,
528 | },
529 | ]
530 | `;
531 |
532 | exports[`moveTile 세로 8 8 4 4 y=1 1`] = `
533 | Array [
534 | Object {
535 | "isDisabled": true,
536 | "value": 4,
537 | "x": 1,
538 | "y": 4,
539 | },
540 | Object {
541 | "isDisabled": true,
542 | "value": 4,
543 | "x": 1,
544 | "y": 4,
545 | },
546 | Object {
547 | "isDisabled": true,
548 | "value": 8,
549 | "x": 1,
550 | "y": 3,
551 | },
552 | Object {
553 | "isDisabled": true,
554 | "value": 8,
555 | "x": 1,
556 | "y": 3,
557 | },
558 | Object {
559 | "isMerged": true,
560 | "value": 8,
561 | "x": 1,
562 | "y": 4,
563 | },
564 | Object {
565 | "isMerged": true,
566 | "value": 16,
567 | "x": 1,
568 | "y": 3,
569 | },
570 | ]
571 | `;
572 |
--------------------------------------------------------------------------------
/game2048/final/src/util/assert.js:
--------------------------------------------------------------------------------
1 | export const assert = function (condition, message) {
2 | if (!condition) {
3 | throw new Error(`Assertion failed: ${message}`);
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/game2048/final/src/util/keyboard.js:
--------------------------------------------------------------------------------
1 | import hotkeys from 'hotkeys-js';
2 |
3 | const observerMap = {};
4 | export function addKeyCallback(key, callback) {
5 | if (!observerMap[key]) {
6 | observerMap[key] = [];
7 | hotkeys(key, () => executeCallbacks(key));
8 | }
9 | observerMap[key].push(callback);
10 | }
11 | export function removeKeyCallback(key, callback) {
12 | observerMap[key] = observerMap[key].filter(item => item !== callback);
13 | }
14 |
15 | function executeCallbacks(key) {
16 | for (const ob of observerMap[key]) {
17 | ob();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/game2048/final/src/util/number.js:
--------------------------------------------------------------------------------
1 | export function getRandomInteger(from, to) {
2 | return Math.floor(Math.random() * to + from);
3 | }
4 |
--------------------------------------------------------------------------------
/game2048/final/src/util/tile.js:
--------------------------------------------------------------------------------
1 | import { getRandomInteger } from './number';
2 | import { MAX_POS } from '../constant';
3 | import { assert } from './assert';
4 |
5 | export function getInitialTileList() {
6 | const tileList = [];
7 | const tile1 = makeTile(tileList);
8 | tileList.push(tile1);
9 | const tile2 = makeTile(tileList);
10 | tileList.push(tile2);
11 | return tileList;
12 | }
13 | function checkCollision(tileList, newTile) {
14 | return tileList.some(tile => tile.x === newTile.x && tile.y === newTile.y);
15 | }
16 | let currentId = 0;
17 | export function makeTile(tileList) {
18 | let newTile;
19 | while (!newTile || (tileList && checkCollision(tileList, newTile))) {
20 | newTile = {
21 | id: currentId++,
22 | x: getRandomInteger(1, MAX_POS),
23 | y: getRandomInteger(1, MAX_POS),
24 | value: 2,
25 | isNew: undefined,
26 | isMerged: undefined,
27 | };
28 | }
29 | return newTile;
30 | }
31 |
32 | export function moveTile({ tileList, x, y }) {
33 | assert(x === 0 || y === 0, '');
34 | const isMoveY = y !== 0;
35 | const isMinus = x + y < 0;
36 | const sorted = tileList
37 | .map(item => ({ ...item, isMerged: false, isNew: false }))
38 | .filter(item => !item.isDisabled)
39 | .sort((a, b) => {
40 | const res = isMoveY ? a.x - b.x : a.y - b.y;
41 | if (res) {
42 | return res;
43 | } else {
44 | if (isMoveY) {
45 | return isMinus ? a.y - b.y : b.y - a.y;
46 | } else {
47 | return isMinus ? a.x - b.x : b.x - a.x;
48 | }
49 | }
50 | });
51 | const initialPos = isMinus ? 1 : MAX_POS;
52 | let pos = initialPos;
53 | for (let i = 0; i < sorted.length; i++) {
54 | if (isMoveY) {
55 | sorted[i].y = pos;
56 | pos = isMinus ? pos + 1 : pos - 1;
57 | if (sorted[i].x !== sorted[i + 1]?.x) {
58 | pos = initialPos;
59 | }
60 | } else {
61 | sorted[i].x = pos;
62 | pos = isMinus ? pos + 1 : pos - 1;
63 | if (sorted[i].y !== sorted[i + 1]?.y) {
64 | pos = initialPos;
65 | }
66 | }
67 | }
68 |
69 | let nextPos = 0;
70 | const newTileList = [...sorted];
71 | for (let i = 0; i < sorted.length; i++) {
72 | if (sorted[i].isDisabled) {
73 | continue;
74 | }
75 |
76 | if (
77 | nextPos &&
78 | (isMoveY
79 | ? sorted[i].x === sorted[i - 1]?.x
80 | : sorted[i].y === sorted[i - 1]?.y)
81 | ) {
82 | if (isMoveY) {
83 | sorted[i].y = nextPos;
84 | } else {
85 | sorted[i].x = nextPos;
86 | }
87 | nextPos += isMinus ? 1 : -1;
88 | } else {
89 | nextPos = 0;
90 | }
91 |
92 | if (
93 | (isMoveY
94 | ? sorted[i].x === sorted[i + 1]?.x
95 | : sorted[i].y === sorted[i + 1]?.y) &&
96 | sorted[i].value === sorted[i + 1]?.value
97 | ) {
98 | const tile = makeTile();
99 | tile.x = sorted[i].x;
100 | tile.y = sorted[i].y;
101 | tile.isMerged = true;
102 | tile.value = sorted[i].value * 2;
103 | newTileList.push(tile);
104 | sorted[i].isDisabled = true;
105 | sorted[i + 1].isDisabled = true;
106 | if (isMoveY) {
107 | nextPos = sorted[i + 1].y;
108 | sorted[i + 1].y = sorted[i].y;
109 | } else {
110 | nextPos = sorted[i + 1].x;
111 | sorted[i + 1].x = sorted[i].x;
112 | }
113 | }
114 | }
115 | return newTileList;
116 | }
117 |
--------------------------------------------------------------------------------
/game2048/final/src/util/tile.test.js:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { moveTile } from './tile';
3 |
4 | describe('moveTile', () => {
5 | function removeProps(tileList) {
6 | for (const tile of tileList) {
7 | delete tile.id;
8 | if (tile.isMerged === false) {
9 | delete tile.isMerged;
10 | }
11 | if (tile.isNew === false) {
12 | delete tile.isNew;
13 | }
14 | }
15 | return tileList;
16 | }
17 | describe('가로 8 8 4 4', () => {
18 | const tileList = [
19 | {
20 | id: 11,
21 | x: 1,
22 | y: 1,
23 | value: 8,
24 | },
25 | {
26 | id: 12,
27 | x: 2,
28 | y: 1,
29 | value: 8,
30 | },
31 | {
32 | id: 13,
33 | x: 3,
34 | y: 1,
35 | value: 4,
36 | },
37 | {
38 | id: 14,
39 | x: 4,
40 | y: 1,
41 | value: 4,
42 | },
43 | ];
44 | it('x=1', () => {
45 | expect(removeProps(moveTile({ x: 1, y: 0, tileList }))).toMatchSnapshot();
46 | });
47 | it('x=-1', () => {
48 | expect(
49 | removeProps(moveTile({ x: -1, y: 0, tileList })),
50 | ).toMatchSnapshot();
51 | });
52 | it('y=1', () => {
53 | expect(removeProps(moveTile({ x: 0, y: 1, tileList }))).toMatchSnapshot();
54 | });
55 | it('y=-1', () => {
56 | expect(
57 | removeProps(moveTile({ x: 0, y: -1, tileList })),
58 | ).toMatchSnapshot();
59 | });
60 | });
61 | describe('세로 8 8 4 4', () => {
62 | const tileList = [
63 | {
64 | id: 11,
65 | x: 1,
66 | y: 1,
67 | value: 8,
68 | },
69 | {
70 | id: 12,
71 | x: 1,
72 | y: 2,
73 | value: 8,
74 | },
75 | {
76 | id: 13,
77 | x: 1,
78 | y: 3,
79 | value: 4,
80 | },
81 | {
82 | id: 14,
83 | x: 1,
84 | y: 4,
85 | value: 4,
86 | },
87 | ];
88 | it('x=1', () => {
89 | expect(removeProps(moveTile({ x: 1, y: 0, tileList }))).toMatchSnapshot();
90 | });
91 | it('x=-1', () => {
92 | expect(
93 | removeProps(moveTile({ x: -1, y: 0, tileList })),
94 | ).toMatchSnapshot();
95 | });
96 | it('y=1', () => {
97 | expect(removeProps(moveTile({ x: 0, y: 1, tileList }))).toMatchSnapshot();
98 | });
99 | it('y=-1', () => {
100 | expect(
101 | removeProps(moveTile({ x: 0, y: -1, tileList })),
102 | ).toMatchSnapshot();
103 | });
104 | });
105 | describe('가로 2 4 8 8', () => {
106 | const tileList = [
107 | {
108 | id: 11,
109 | x: 1,
110 | y: 1,
111 | value: 2,
112 | },
113 | {
114 | id: 12,
115 | x: 2,
116 | y: 1,
117 | value: 4,
118 | },
119 | {
120 | id: 13,
121 | x: 3,
122 | y: 1,
123 | value: 8,
124 | },
125 | {
126 | id: 14,
127 | x: 4,
128 | y: 1,
129 | value: 8,
130 | },
131 | ];
132 | it('x=1', () => {
133 | expect(removeProps(moveTile({ x: 1, y: 0, tileList }))).toMatchSnapshot();
134 | });
135 | it('x=-1', () => {
136 | expect(
137 | removeProps(moveTile({ x: -1, y: 0, tileList })),
138 | ).toMatchSnapshot();
139 | });
140 | });
141 | describe('가로 2 x 8 8', () => {
142 | const tileList = [
143 | {
144 | id: 11,
145 | x: 1,
146 | y: 1,
147 | value: 2,
148 | },
149 | {
150 | id: 13,
151 | x: 3,
152 | y: 1,
153 | value: 8,
154 | },
155 | {
156 | id: 14,
157 | x: 4,
158 | y: 1,
159 | value: 8,
160 | },
161 | ];
162 | it('x=1', () => {
163 | expect(removeProps(moveTile({ x: 1, y: 0, tileList }))).toMatchSnapshot();
164 | });
165 | it('x=-1', () => {
166 | expect(
167 | removeProps(moveTile({ x: -1, y: 0, tileList })),
168 | ).toMatchSnapshot();
169 | });
170 | });
171 | describe('세로 2 x 8 8', () => {
172 | const tileList = [
173 | {
174 | id: 11,
175 | x: 1,
176 | y: 1,
177 | value: 2,
178 | },
179 | {
180 | id: 13,
181 | x: 1,
182 | y: 3,
183 | value: 8,
184 | },
185 | {
186 | id: 14,
187 | x: 1,
188 | y: 4,
189 | value: 8,
190 | },
191 | ];
192 | it('y=1', () => {
193 | expect(removeProps(moveTile({ x: 0, y: 1, tileList }))).toMatchSnapshot();
194 | });
195 | it('y=-1', () => {
196 | expect(
197 | removeProps(moveTile({ x: 0, y: -1, tileList })),
198 | ).toMatchSnapshot();
199 | });
200 | });
201 | describe('가로 여러 줄, 2 2 4 4, 8 x 4 4', () => {
202 | const tileList = [
203 | {
204 | id: 11,
205 | x: 1,
206 | y: 1,
207 | value: 2,
208 | },
209 | {
210 | id: 12,
211 | x: 2,
212 | y: 1,
213 | value: 2,
214 | },
215 | {
216 | id: 13,
217 | x: 3,
218 | y: 1,
219 | value: 4,
220 | },
221 | {
222 | id: 14,
223 | x: 4,
224 | y: 1,
225 | value: 4,
226 | },
227 | {
228 | id: 15,
229 | x: 1,
230 | y: 2,
231 | value: 8,
232 | },
233 | {
234 | id: 16,
235 | x: 3,
236 | y: 2,
237 | value: 4,
238 | },
239 | {
240 | id: 17,
241 | x: 4,
242 | y: 2,
243 | value: 4,
244 | },
245 | ];
246 | it('x=1', () => {
247 | expect(removeProps(moveTile({ x: 1, y: 0, tileList }))).toMatchSnapshot();
248 | });
249 | it('x=-1', () => {
250 | expect(
251 | removeProps(moveTile({ x: -1, y: 0, tileList })),
252 | ).toMatchSnapshot();
253 | });
254 | });
255 | });
256 |
--------------------------------------------------------------------------------
/game2048/start/.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 |
--------------------------------------------------------------------------------
/game2048/start/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "module": "commonjs",
5 | "target": "es6",
6 | "checkJs": true
7 | },
8 | "exclude": ["node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/game2048/start/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "game2048",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.5.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "classnames": "^2.2.6",
10 | "hotkeys-js": "^3.8.1",
11 | "lodash": "^4.17.19",
12 | "react": "^16.13.1",
13 | "react-dom": "^16.13.1",
14 | "react-scripts": "3.4.1"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": "react-app"
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/game2048/start/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/game2048/start/public/favicon.ico
--------------------------------------------------------------------------------
/game2048/start/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/game2048/start/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/game2048/start/public/logo192.png
--------------------------------------------------------------------------------
/game2048/start/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/game2048/start/public/logo512.png
--------------------------------------------------------------------------------
/game2048/start/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/game2048/start/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/game2048/start/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from './component/Header';
3 | import AboveGame from './component/AboveGame';
4 | import Game from './component/Game';
5 |
6 | export default function App() {
7 | return (
8 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/game2048/start/src/component/AboveGame.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function AboveGame() {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/game2048/start/src/component/Game.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function Game() {
4 | return (
5 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/game2048/start/src/component/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function Header() {
4 | return (
5 |
6 | 2048
7 |
8 |
9 | 0
10 |
11 |
2480
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/game2048/start/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import './index.css';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
--------------------------------------------------------------------------------
/game2048/start/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/ts-todo/final/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todo-list",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "nodemon --watch '*.ts' --exec 'ts-node' src/index.ts",
8 | "build": "tsc"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "@types/node": "^14.6.0",
15 | "chalk": "^4.1.0",
16 | "nodemon": "^2.0.4",
17 | "ts-node": "^9.0.0",
18 | "typescript": "^4.0.2"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/ts-todo/final/src/Command.ts:
--------------------------------------------------------------------------------
1 | import { waitForInput } from './Input';
2 | import {
3 | PRIORITY_NAME_MAP,
4 | Priority,
5 | Action,
6 | ActionNewTodo,
7 | AppState,
8 | ActionDeleteTodo,
9 | } from './type';
10 | import { getIsValidEnumValue } from './util';
11 | import chalk from 'chalk';
12 |
13 | export abstract class Command {
14 | constructor(public key: string, private desc: string) {}
15 | toString() {
16 | return chalk`{blue.bold ${this.key}}: ${this.desc}`;
17 | }
18 | abstract async run(state: AppState): Promise;
19 | }
20 |
21 | export class CommandPrintTodos extends Command {
22 | constructor() {
23 | super('p', chalk`모든 할 일 {red.bold 출력}하기`);
24 | }
25 | async run(state: AppState): Promise {
26 | for (const todo of state.todos) {
27 | const text = todo.toString();
28 | console.log(text);
29 | }
30 | await waitForInput('press any key: ');
31 | }
32 | }
33 |
34 | export class CommandDeleteTodo extends Command {
35 | constructor() {
36 | super('d', chalk`할 일 {red.bold 제거}하기`);
37 | }
38 | async run(state: AppState): Promise {
39 | for (const todo of state.todos) {
40 | const text = todo.toString();
41 | console.log(text);
42 | }
43 | const idStr = await waitForInput('press todo id to delete: ');
44 | const id = Number(idStr);
45 | return {
46 | type: 'deleteTodo',
47 | id,
48 | };
49 | }
50 | }
51 |
52 | export class CommandNewTodo extends Command {
53 | constructor() {
54 | super('n', chalk`할 일 {red.bold 추가}하기`);
55 | }
56 | async run(): Promise {
57 | const title = await waitForInput('title: ');
58 | const priorityStr = await waitForInput(
59 | `priority ${PRIORITY_NAME_MAP[Priority.High]}(${Priority.High}) ~ ${
60 | PRIORITY_NAME_MAP[Priority.Low]
61 | }(${Priority.Low}): `,
62 | );
63 | const priority = Number(priorityStr);
64 | if (title && CommandNewTodo.getIsPriority(priority)) {
65 | return {
66 | type: 'newTodo',
67 | title,
68 | priority,
69 | };
70 | }
71 | }
72 |
73 | static getIsPriority(priority: number): priority is Priority {
74 | return getIsValidEnumValue(Priority, priority);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/ts-todo/final/src/Input.ts:
--------------------------------------------------------------------------------
1 | import readline from 'readline';
2 |
3 | const readlineInterface = readline.createInterface({
4 | input: process.stdin,
5 | output: process.stdout,
6 | });
7 |
8 | export function waitForInput(msg: string) {
9 | return new Promise(res =>
10 | readlineInterface.question(msg, key => {
11 | res(key);
12 | }),
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/ts-todo/final/src/Todo.ts:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 | import { Priority, PRIORITY_NAME_MAP } from './type';
3 |
4 | export default class Todo {
5 | static nextId: number = 1;
6 | constructor(
7 | private title: string,
8 | private priority: Priority,
9 | public id: number = Todo.nextId,
10 | ) {
11 | Todo.nextId++;
12 | }
13 | toString() {
14 | return chalk`{blue.bold ${this.id})} 제목: {bold ${
15 | this.title
16 | }} (우선순위: {${PRIORITY_STYLE_MAP[this.priority]} ${
17 | PRIORITY_NAME_MAP[this.priority]
18 | }})`;
19 | }
20 | }
21 |
22 | const PRIORITY_STYLE_MAP: { [key in Priority]: string } = {
23 | [Priority.High]: 'red.bold',
24 | [Priority.Medium]: 'grey.bold',
25 | [Priority.Low]: 'yellow.bold',
26 | };
27 |
--------------------------------------------------------------------------------
/ts-todo/final/src/index.ts:
--------------------------------------------------------------------------------
1 | import Todo from './Todo';
2 | import { waitForInput } from './Input';
3 | import {
4 | Command,
5 | CommandNewTodo,
6 | CommandPrintTodos,
7 | CommandDeleteTodo,
8 | } from './Command';
9 | import { Priority, AppState, Action } from './type';
10 |
11 | const commands: Array = [
12 | new CommandPrintTodos(),
13 | new CommandNewTodo(),
14 | new CommandDeleteTodo(),
15 | ];
16 |
17 | async function main() {
18 | let state: AppState = {
19 | todos: [
20 | new Todo('test1', Priority.High),
21 | new Todo('test2', Priority.Medium),
22 | new Todo('test3', Priority.Low),
23 | ],
24 | };
25 | while (true) {
26 | console.clear();
27 | for (const command of commands) {
28 | console.log(command.toString());
29 | }
30 | console.log();
31 | const key = await waitForInput(`input command: `);
32 | console.clear();
33 | const command = commands.find(item => item.key === key);
34 | if (command) {
35 | const action = await command.run(state);
36 | if (action) {
37 | state = getNextState(state, action);
38 | }
39 | }
40 | }
41 | }
42 | main();
43 |
44 | function getNextState(state: AppState, action: Action): AppState {
45 | switch (action.type) {
46 | case 'newTodo':
47 | return {
48 | ...state,
49 | todos: [...state.todos, new Todo(action.title, action.priority)],
50 | };
51 | case 'deleteTodo':
52 | return {
53 | ...state,
54 | todos: state.todos.filter(item => item.id !== action.id),
55 | };
56 | default:
57 | return state;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/ts-todo/final/src/type.ts:
--------------------------------------------------------------------------------
1 | import Todo from './Todo';
2 |
3 | export interface AppState {
4 | todos: Todo[];
5 | }
6 |
7 | export interface ActionNewTodo {
8 | type: 'newTodo';
9 | title: string;
10 | priority: Priority;
11 | }
12 | export interface ActionDeleteTodo {
13 | type: 'deleteTodo';
14 | id: number;
15 | }
16 | export type Action = ActionNewTodo | ActionDeleteTodo;
17 |
18 | export enum Priority {
19 | High,
20 | Medium,
21 | Low,
22 | }
23 |
24 | export const PRIORITY_NAME_MAP: { [key in Priority]: string } = {
25 | [Priority.High]: '높음',
26 | [Priority.Medium]: '중간',
27 | [Priority.Low]: '낮음',
28 | };
29 |
--------------------------------------------------------------------------------
/ts-todo/final/src/util.ts:
--------------------------------------------------------------------------------
1 | export function getIsValidEnumValue(enumObject: any, value: number | string) {
2 | return Object.keys(enumObject)
3 | .filter(key => isNaN(Number(key)))
4 | .some(key => enumObject[key] === value);
5 | }
6 |
--------------------------------------------------------------------------------
/ts-todo/final/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
9 | // "lib": [], /* Specify library files to be included in the compilation. */
10 | // "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | "outDir": "dist" /* Redirect output structure to the directory. */,
18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 | // "removeComments": true, /* Do not emit comments to output. */
22 | // "noEmit": true, /* Do not emit outputs. */
23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 |
27 | /* Strict Type-Checking Options */
28 | "strict": true /* Enable all strict type-checking options. */,
29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 | // "strictNullChecks": true, /* Enable strict null checks. */
31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 |
37 | /* Additional Checks */
38 | // "noUnusedLocals": true, /* Report errors on unused locals. */
39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 |
43 | /* Module Resolution Options */
44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
48 | // "typeRoots": [], /* List of folders to include type definitions from. */
49 | // "types": [], /* Type declaration files to be included in compilation. */
50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
54 |
55 | /* Source Map Options */
56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
60 |
61 | /* Experimental Options */
62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
64 |
65 | /* Advanced Options */
66 | "skipLibCheck": true /* Skip type checking of declaration files. */,
67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/ts-todo/start/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todo-list",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "nodemon --watch '*.ts' --exec 'ts-node' src/index.ts",
8 | "build": "tsc"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "@types/node": "^14.6.0",
15 | "chalk": "^4.1.0",
16 | "nodemon": "^2.0.4",
17 | "ts-node": "^9.0.0",
18 | "typescript": "^4.0.2"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/ts-todo/start/src/Input.ts:
--------------------------------------------------------------------------------
1 | import readline from 'readline';
2 |
3 | const readlineInterface = readline.createInterface({
4 | input: process.stdin,
5 | output: process.stdout,
6 | });
7 |
8 | export function waitForInput(msg: string) {
9 | return new Promise(res =>
10 | readlineInterface.question(msg, key => {
11 | res(key);
12 | }),
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/ts-todo/start/src/index.ts:
--------------------------------------------------------------------------------
1 | console.log('hello world');
2 |
--------------------------------------------------------------------------------
/ts-todo/start/src/util.ts:
--------------------------------------------------------------------------------
1 | export function getIsValidEnumValue(enumObject: any, value: number | string) {
2 | return Object.keys(enumObject)
3 | .filter(key => isNaN(Number(key)))
4 | .some(key => enumObject[key] === value);
5 | }
6 |
--------------------------------------------------------------------------------
/ts-todo/start/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
9 | // "lib": [], /* Specify library files to be included in the compilation. */
10 | // "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | "outDir": "dist" /* Redirect output structure to the directory. */,
18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 | // "removeComments": true, /* Do not emit comments to output. */
22 | // "noEmit": true, /* Do not emit outputs. */
23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 |
27 | /* Strict Type-Checking Options */
28 | "strict": true /* Enable all strict type-checking options. */,
29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 | // "strictNullChecks": true, /* Enable strict null checks. */
31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 |
37 | /* Additional Checks */
38 | // "noUnusedLocals": true, /* Report errors on unused locals. */
39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 |
43 | /* Module Resolution Options */
44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
48 | // "typeRoots": [], /* List of folders to include type definitions from. */
49 | // "types": [], /* Type declaration files to be included in compilation. */
50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
54 |
55 | /* Source Map Options */
56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
60 |
61 | /* Experimental Options */
62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
64 |
65 | /* Advanced Options */
66 | "skipLibCheck": true /* Skip type checking of declaration files. */,
67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/whois/final/.env.development:
--------------------------------------------------------------------------------
1 | REACT_APP_API_HOST=http://localhost:3001
--------------------------------------------------------------------------------
/whois/final/.env.production:
--------------------------------------------------------------------------------
1 | REACT_APP_API_HOST=http://localhost:3001
--------------------------------------------------------------------------------
/whois/final/.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 |
--------------------------------------------------------------------------------
/whois/final/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "module": "commonjs",
5 | "target": "es2020",
6 | "checkJs": true
7 | },
8 | "exclude": ["node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/whois/final/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "whois",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.5.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "react": "^16.13.1",
10 | "react-dom": "^16.13.1",
11 | "react-scripts": "3.4.1",
12 | "@ant-design/icons": "^4.2.1",
13 | "@testing-library/react-hooks": "^3.3.0",
14 | "@types/jest": "^26.0.4",
15 | "antd": "^4.4.2",
16 | "axios": "^0.19.2",
17 | "diff": "^4.0.2",
18 | "react-redux": "^7.2.0",
19 | "react-router-dom": "^5.2.0",
20 | "react-test-renderer": "^16.13.1",
21 | "redux": "^4.0.5",
22 | "redux-saga": "^1.1.3"
23 | },
24 | "scripts": {
25 | "start": "react-scripts start",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test",
28 | "eject": "react-scripts eject"
29 | },
30 | "eslintConfig": {
31 | "extends": "react-app"
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/whois/final/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/whois/final/public/favicon.ico
--------------------------------------------------------------------------------
/whois/final/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
73 |
81 |
82 |
83 |
84 |
85 |
86 |
Downloading...
87 |
92 |
93 |
98 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/whois/final/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/whois/final/public/logo192.png
--------------------------------------------------------------------------------
/whois/final/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/whois/final/public/logo512.png
--------------------------------------------------------------------------------
/whois/final/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/whois/final/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/whois/final/server/data.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/whois/final/server/data.db
--------------------------------------------------------------------------------
/whois/final/server/db.js:
--------------------------------------------------------------------------------
1 | const sqlite3 = require('sqlite3');
2 |
3 | // const db = new sqlite3.Database(':memory:');
4 | const db = new sqlite3.Database('./data.db', sqlite3.OPEN_READWRITE);
5 |
6 | const users = [
7 | ['land', '글로벌웹', '팀장, 웹, 결제, 리액트'],
8 | ['bono', '글로벌웹', '팀원, 로그인, 작품홈'],
9 | ['shai', '국내웹', '팀장, 비디오 플레이어, 카톡더보기'],
10 | ];
11 | const placeholders = users.map(_ => '(?,?,?)').join(',');
12 | const sql = 'INSERT INTO user(name, department, tag) VALUES ' + placeholders;
13 | db.run(
14 | sql,
15 | users.flatMap(_ => _),
16 | function (err) {
17 | if (err) {
18 | return console.error(err.message);
19 | }
20 | console.log(`Rows inserted ${this.changes}`);
21 | },
22 | );
23 |
24 | db.close();
25 |
--------------------------------------------------------------------------------
/whois/final/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const sqlite3 = require('sqlite3');
3 | const cors = require('cors');
4 | const bodyParser = require('body-parser');
5 | const cookieParser = require('cookie-parser');
6 |
7 | const app = express();
8 | app.use(
9 | cors({
10 | origin: 'http://localhost:3000',
11 | credentials: true,
12 | }),
13 | );
14 | app.use(bodyParser.json());
15 | app.use(cookieParser());
16 |
17 | const db = new sqlite3.Database('./data.db', sqlite3.OPEN_READWRITE);
18 | app.get('/user/search', (req, res) => {
19 | setTimeout(() => {
20 | const keyword = req.query.keyword;
21 | db.all(
22 | `SELECT * FROM user where name like '%${keyword}%' or department like '%${keyword}%' or tag like '%${keyword}%'`,
23 | [],
24 | (err, rows) => {
25 | if (err) {
26 | throw err;
27 | }
28 | res.send(makeResponse({ data: rows }));
29 | },
30 | );
31 | }, 1);
32 | });
33 | app.get('/history', (req, res) => {
34 | setTimeout(() => {
35 | const { name, page = 0 } = req.query;
36 | // @ts-ignore
37 | const pagination = `limit ${PAGING_SIZE} offset ${PAGING_SIZE * page}`;
38 | const sql = name
39 | ? `SELECT * FROM history where name='${name}' order by date DESC ${pagination}`
40 | : `SELECT * FROM history order by date DESC ${pagination}`;
41 | db.all(sql, [], (err, rows) => {
42 | if (err) {
43 | throw err;
44 | }
45 | db.all('SELECT count(*) as totalCount FROM history', [], (err, rows2) => {
46 | const totalCount = rows2[0].totalCount;
47 | res.send(makeResponse({ data: rows, totalCount }));
48 | });
49 | });
50 | }, 1);
51 | });
52 | app.post('/user/update', (req, res) => {
53 | setTimeout(() => {
54 | const { key, name, value, oldValue } = req.body;
55 | const sql = `UPDATE user SET ${key} = ? WHERE name = ?`;
56 | db.run(sql, [value, name], function (err) {
57 | if (err) {
58 | return console.error(err.message);
59 | }
60 |
61 | const date = new Date(new Date().getTime() + 9 * 3600 * 1000);
62 | const iso = date.toISOString();
63 | const dateStr = `${iso.substr(0, 10)} ${iso.substr(11, 8)}`;
64 | const editor = req.cookies.token || 'unknown';
65 | const history = {
66 | editor,
67 | name,
68 | column: key,
69 | before: oldValue,
70 | after: value,
71 | date: dateStr,
72 | };
73 | const sql = `INSERT INTO history(editor, name, column, before, after, date) VALUES (?,?,?,?,?,?)`;
74 | db.run(
75 | sql,
76 | [
77 | history.editor,
78 | history.name,
79 | history.column,
80 | history.before,
81 | history.after,
82 | history.date,
83 | ],
84 | function (err) {
85 | if (err) {
86 | return console.error(err.message);
87 | }
88 | history.id = this.lastID;
89 | res.send(makeResponse({ data: { history } }));
90 | },
91 | );
92 | });
93 | }, 1);
94 | });
95 |
96 | app.get('/auth/user', (req, res) => {
97 | setTimeout(() => {
98 | const name = req.cookies.token;
99 | res.send(makeResponse({ data: { name } }));
100 | }, 1);
101 | });
102 |
103 | app.post('/auth/login', (req, res) => {
104 | setTimeout(() => {
105 | const { name } = req.body;
106 | db.all(`SELECT * FROM user where name='${name}'`, [], (err, rows) => {
107 | if (err) {
108 | throw err;
109 | }
110 | if (rows.length) {
111 | res.cookie('token', name, {
112 | maxAge: COOKIE_MAX_AGE,
113 | httpOnly: true,
114 | });
115 | res.send(makeResponse({ data: { name } }));
116 | } else {
117 | res.send(
118 | makeResponse({
119 | resultCode: -1,
120 | resultMessage: '존재하지 않는 사용자입니다.',
121 | }),
122 | );
123 | }
124 | });
125 | }, 1);
126 | });
127 |
128 | app.get('/auth/logout', (req, res) => {
129 | setTimeout(() => {
130 | res.cookie('token', '', {
131 | maxAge: 0,
132 | httpOnly: true,
133 | });
134 | res.send(makeResponse({}));
135 | }, 1);
136 | });
137 |
138 | app.post('/auth/signup', (req, res) => {
139 | setTimeout(() => {
140 | const { email } = req.body;
141 | if (!email.includes('@')) {
142 | res.send(
143 | makeResponse({
144 | resultCode: -1,
145 | resultMessage: '이메일 형식이 아닙니다.',
146 | }),
147 | );
148 | return;
149 | }
150 | const name = email.substr(0, email.lastIndexOf('@'));
151 | db.all(`SELECT * FROM user where name='${name}'`, [], (err, rows) => {
152 | if (err) {
153 | throw err;
154 | }
155 | console.log('rows', rows, rows[0]);
156 | if (rows.length) {
157 | res.send(
158 | makeResponse({
159 | resultCode: -1,
160 | resultMessage: '이미 존재하는 사용자입니다.',
161 | }),
162 | );
163 | } else {
164 | const sql = `INSERT INTO user(name, department, tag) VALUES (?,?,?)`;
165 | db.run(sql, [name, '소속없음', ''], function (err) {
166 | if (err) {
167 | return console.error(err.message);
168 | }
169 | res.cookie('token', name, { maxAge: COOKIE_MAX_AGE, httpOnly: true });
170 | res.send(makeResponse({ data: { name } }));
171 | });
172 | }
173 | });
174 | }, 1);
175 | });
176 |
177 | const COOKIE_MAX_AGE = 3600000 * 24 * 14;
178 | const PAGING_SIZE = 20;
179 |
180 | /**
181 | *
182 | * @param {object} param
183 | * @param {object=} param.data
184 | * @param {number=} param.totalCount
185 | * @param {number=} param.resultCode
186 | * @param {string=} param.resultMessage
187 | */
188 | function makeResponse({ data, totalCount, resultCode, resultMessage }) {
189 | return {
190 | data,
191 | totalCount,
192 | resultCode: resultCode || 0,
193 | resultMessage: resultMessage || '',
194 | };
195 | }
196 |
197 | const PORT = 3001;
198 | app.listen(PORT, () => console.log(`app listening on port ${PORT}!`));
199 |
--------------------------------------------------------------------------------
/whois/final/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "nodemon index.js"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "body-parser": "^1.19.0",
14 | "cookie-parser": "^1.4.5",
15 | "cors": "^2.8.5",
16 | "express": "^4.17.1",
17 | "lru-cache": "^6.0.0",
18 | "nodemon": "^2.0.4",
19 | "sqlite3": "^5.1.4"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/whois/final/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import Search from './search/container/Search';
3 | import 'antd/dist/antd.css';
4 | import { Route } from 'react-router-dom';
5 | import User from './user/container/User';
6 | import Login from './auth/container/Login';
7 | import Signup from './auth/container/Signup';
8 | import { useDispatch } from 'react-redux';
9 | import { actions as authActions } from './auth/state';
10 |
11 | export default function App() {
12 | useEffect(() => {
13 | const bodyEl = document.getElementsByTagName('body')[0];
14 | const loadingEl = document.getElementById('init-loading');
15 | bodyEl.removeChild(loadingEl);
16 | }, []);
17 | const dispatch = useDispatch();
18 | useEffect(() => {
19 | dispatch(authActions.fetchUser());
20 | }, [dispatch]);
21 | return (
22 | <>
23 |
24 |
25 |
26 |
27 | >
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/whois/final/src/auth/component/AuthLayout.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Col, Row, Typography, Form } from 'antd';
3 |
4 | /**
5 | *
6 | * @param {object} param
7 | * @param {() => void} param.onFinish
8 | * @param {import('react').ReactNode} param.children
9 | */
10 | export default function AuthLayout({ children, onFinish }) {
11 | return (
12 | <>
13 |
14 |
15 |
16 | 찾 아 야 한 다
17 |
18 |
19 |
20 |
21 |
22 |
29 |
30 |
31 | >
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/whois/final/src/auth/container/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form, Input, Button } from 'antd';
3 | import { UserOutlined, LockOutlined } from '@ant-design/icons';
4 | import { Link } from 'react-router-dom';
5 | import AuthLayout from '../component/AuthLayout';
6 | import { useDispatch } from 'react-redux';
7 | import { actions } from '../state';
8 | import useBlockLoginUser from '../hook/useBlockLoginUser';
9 |
10 | export default function Login() {
11 | useBlockLoginUser();
12 | const dispatch = useDispatch();
13 | function onFinish({ username, password }) {
14 | dispatch(actions.fetchLogin(username, password));
15 | }
16 | return (
17 |
18 |
22 | } placeholder="Username" />
23 |
24 |
28 | }
30 | type="password"
31 | placeholder="Password"
32 | />
33 |
34 |
35 |
38 | Or register now!
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/whois/final/src/auth/container/Signup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AuthLayout from '../component/AuthLayout';
3 | import { Input, Button, Form } from 'antd';
4 | import { Link } from 'react-router-dom';
5 | import { useDispatch } from 'react-redux';
6 | import { actions } from '../state';
7 | import useBlockLoginUser from '../hook/useBlockLoginUser';
8 |
9 | export default function Signup() {
10 | useBlockLoginUser();
11 | const dispatch = useDispatch();
12 | function onFinish({ name }) {
13 | const email = `${name}${EMAIL_SUFFIX}`;
14 | dispatch(actions.fetchSignup(email));
15 | }
16 | return (
17 |
18 |
27 |
28 |
29 |
30 |
33 | Or login
34 |
35 |
36 | );
37 | }
38 |
39 | const EMAIL_SUFFIX = '@company.com';
40 |
--------------------------------------------------------------------------------
/whois/final/src/auth/hook/useBlockLoginUser.js:
--------------------------------------------------------------------------------
1 | import { useHistory } from 'react-router-dom';
2 | import { useSelector } from 'react-redux';
3 | import { useEffect } from 'react';
4 | import { AuthStatus } from '../../common/constant';
5 |
6 | export default function useBlockLoginUser() {
7 | const history = useHistory();
8 | const status = useSelector(state => state.auth.status);
9 | useEffect(() => {
10 | if (status === AuthStatus.Login) {
11 | history.replace('/');
12 | }
13 | }, [status, history]);
14 | }
15 |
--------------------------------------------------------------------------------
/whois/final/src/auth/state/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | createReducer,
3 | createSetValueAction,
4 | setValueReducer,
5 | } from '../../common/redux-helper';
6 | import { AuthStatus } from '../../common/constant';
7 |
8 | export const Types = {
9 | SetValue: 'auth/SetValue',
10 | FetchLogin: 'auth/FetchLogin',
11 | SetUser: 'auth/SetUser',
12 | FetchSignup: 'auth/FetchSignup',
13 | FetchUser: 'auth/FetchUser',
14 | FetchLogout: 'auth/FetchLogout',
15 | };
16 |
17 | export const actions = {
18 | setValue: createSetValueAction(Types.SetValue),
19 | fetchLogin: (name, password) => ({
20 | type: Types.FetchLogin,
21 | name,
22 | password,
23 | }),
24 | setUser: name => ({
25 | type: Types.SetUser,
26 | name,
27 | }),
28 | fetchSignup: email => ({
29 | type: Types.FetchSignup,
30 | email,
31 | }),
32 | fetchUser: () => ({
33 | type: Types.FetchUser,
34 | }),
35 | fetchLogout: () => ({ type: Types.FetchLogout }),
36 | };
37 |
38 | const INITIAL_STATE = {
39 | name: '',
40 | status: undefined,
41 | };
42 | const reducer = createReducer(INITIAL_STATE, {
43 | [Types.SetValue]: setValueReducer,
44 | [Types.SetUser]: (state, action) => {
45 | state.name = action.name;
46 | state.status = action.name ? AuthStatus.Login : AuthStatus.NotLogin;
47 | },
48 | });
49 | export default reducer;
50 |
--------------------------------------------------------------------------------
/whois/final/src/auth/state/saga.js:
--------------------------------------------------------------------------------
1 | import { all, put, call, takeLeading } from 'redux-saga/effects';
2 | import { actions, Types } from './index';
3 | import { callApi } from '../../common/util/api';
4 | import { makeFetchSaga } from '../../common/util/fetch';
5 |
6 | function* fetchLogin({ name, password }) {
7 | const { isSuccess, data } = yield call(callApi, {
8 | url: '/auth/login',
9 | method: 'post',
10 | data: {
11 | name,
12 | password,
13 | },
14 | });
15 |
16 | if (isSuccess && data) {
17 | yield put(actions.setUser(data.name));
18 | }
19 | }
20 |
21 | function* fetchSignup({ email }) {
22 | const { isSuccess, data } = yield call(callApi, {
23 | url: '/auth/signup',
24 | method: 'post',
25 | data: {
26 | email,
27 | },
28 | });
29 |
30 | if (isSuccess && data) {
31 | yield put(actions.setUser(data.name));
32 | }
33 | }
34 |
35 | function* fetchUser() {
36 | const { isSuccess, data } = yield call(callApi, {
37 | url: '/auth/user',
38 | });
39 |
40 | if (isSuccess && data) {
41 | yield put(actions.setUser(data.name));
42 | }
43 | }
44 |
45 | function* fetchLogout() {
46 | const { isSuccess } = yield call(callApi, {
47 | url: '/auth/logout',
48 | });
49 |
50 | if (isSuccess) {
51 | yield put(actions.setUser(''));
52 | }
53 | }
54 |
55 | export default function* () {
56 | yield all([
57 | takeLeading(
58 | Types.FetchLogin,
59 | makeFetchSaga({ fetchSaga: fetchLogin, canCache: false }),
60 | ),
61 | takeLeading(
62 | Types.FetchSignup,
63 | makeFetchSaga({ fetchSaga: fetchSignup, canCache: false }),
64 | ),
65 | takeLeading(
66 | Types.FetchUser,
67 | makeFetchSaga({ fetchSaga: fetchUser, canCache: false }),
68 | ),
69 | takeLeading(
70 | Types.FetchLogout,
71 | makeFetchSaga({ fetchSaga: fetchLogout, canCache: false }),
72 | ),
73 | ]);
74 | }
75 |
--------------------------------------------------------------------------------
/whois/final/src/common/component/History.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Timeline, Space, Tag, Typography } from 'antd';
3 | import { Link } from 'react-router-dom';
4 | import { diffWords } from 'diff';
5 |
6 | /**
7 | *
8 | * @param {object} param
9 | * @param {object[]} param.items
10 | */
11 | export default function History({ items }) {
12 | return (
13 |
14 | {items.map(item => (
15 |
16 |
17 |
18 |
19 |
20 | 수정한 사람: {item.editor}
21 |
22 |
23 |
24 | 수정된 사람: {item.name}
25 |
26 | 날짜: {item.date}
27 | 속성: {COLUMN_MAP[item.column]}
28 |
29 |
30 | {getDiff(item).map((diff, index) => (
31 |
38 | {diff.value}
39 |
40 | ))}
41 |
42 |
43 |
44 | ))}
45 |
46 | );
47 | }
48 |
49 | const COLUMN_MAP = {
50 | tag: '태그',
51 | department: '소속',
52 | };
53 |
54 | /**
55 | *
56 | * @param {object} param
57 | * @param {'tag' | 'department'} param.column
58 | * @param {string} param.before
59 | * @param {string} param.after
60 | * @returns {Array<{value: string, removed?: boolean, added?: boolean}>}
61 | */
62 | function getDiff({ column, before, after }) {
63 | if (column === 'tag') {
64 | const tags1 = before.split(',').map(item => item.trim());
65 | const tags2 = after.split(',').map(item => item.trim());
66 | if (tags1.length > tags2.length) {
67 | const tag = tags1.find(item => !tags2.includes(item));
68 | if (tag) {
69 | return [{ value: tag, removed: true }];
70 | }
71 | } else if (tags1.length < tags2.length) {
72 | const tag = tags2.find(item => !tags1.includes(item));
73 | if (tag) {
74 | return [{ value: tag, added: true }];
75 | }
76 | }
77 | }
78 |
79 | return diffWords(before, after);
80 | }
81 |
--------------------------------------------------------------------------------
/whois/final/src/common/constant.js:
--------------------------------------------------------------------------------
1 | export const API_HOST = process.env.REACT_APP_API_HOST;
2 | export const FetchStatus = {
3 | Request: 'Request',
4 | Success: 'Success',
5 | Fail: 'Fail',
6 | };
7 | export const AuthStatus = {
8 | Login: 'Login',
9 | NotLogin: 'NotLogin',
10 | };
11 |
--------------------------------------------------------------------------------
/whois/final/src/common/hook/useFetchInfo.js:
--------------------------------------------------------------------------------
1 | import { getFetchKey } from '../util/fetch';
2 | import { useSelector, shallowEqual } from 'react-redux';
3 | import { FetchStatus } from '../constant';
4 | import { FETCH_KEY } from '../redux-helper';
5 |
6 | export default function useFetchInfo(actionType, fetchKey) {
7 | const _fetchKey = getFetchKey({
8 | type: actionType,
9 | [FETCH_KEY]: fetchKey,
10 | });
11 | return useSelector(
12 | state => ({
13 | fetchStatus:
14 | state.common.fetchInfo.fetchStatusMap[actionType]?.[_fetchKey],
15 | isFetching:
16 | state.common.fetchInfo.fetchStatusMap[actionType]?.[_fetchKey] ===
17 | FetchStatus.Request,
18 | isFetched:
19 | state.common.fetchInfo.fetchStatusMap[actionType]?.[_fetchKey] ===
20 | FetchStatus.Success ||
21 | state.common.fetchInfo.fetchStatusMap[actionType]?.[_fetchKey] ===
22 | FetchStatus.Fail,
23 | isSlow: !!state.common.fetchInfo.isSlowMap[actionType]?.[_fetchKey],
24 | nextPage:
25 | state.common.fetchInfo.nextPageMap[actionType]?.[_fetchKey] || 0,
26 | totalCount:
27 | state.common.fetchInfo.totalCountMap[actionType]?.[_fetchKey] || 0,
28 | errorMessage:
29 | state.common.fetchInfo.errorMessageMap[actionType]?.[_fetchKey],
30 | }),
31 | shallowEqual,
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/whois/final/src/common/hook/useNeedLogin.js:
--------------------------------------------------------------------------------
1 | import { useHistory } from 'react-router-dom';
2 | import { useSelector } from 'react-redux';
3 | import { useEffect } from 'react';
4 | import { AuthStatus } from '../constant';
5 |
6 | export default function useNeedLogin() {
7 | const history = useHistory();
8 | const status = useSelector(state => state.auth.status);
9 | useEffect(() => {
10 | if (status === AuthStatus.NotLogin) {
11 | history.replace('/login');
12 | }
13 | }, [status, history]);
14 | }
15 |
--------------------------------------------------------------------------------
/whois/final/src/common/redux-helper.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | export function createReducer(initialState, handlerMap) {
4 | return function (state = initialState, action) {
5 | const handler = handlerMap[action.type];
6 | if (handler) {
7 | if (action[NOT_IMMUTABLE]) {
8 | return handler(state, action);
9 | } else {
10 | return produce(state, draft => {
11 | const handler = handlerMap[action.type];
12 | handler(draft, action);
13 | });
14 | }
15 | } else {
16 | return state;
17 | }
18 | };
19 | }
20 |
21 | export function createSetValueAction(type) {
22 | return (key, value) => ({ type, key, value });
23 | }
24 | export function setValueReducer(state, action) {
25 | state[action.key] = action.value;
26 | }
27 |
28 | export const FETCH_PAGE = Symbol('FETCH_PAGE');
29 | export const FETCH_KEY = Symbol('FETCH_KEY');
30 | export const NOT_IMMUTABLE = Symbol('NOT_IMMUTABLE');
31 |
--------------------------------------------------------------------------------
/whois/final/src/common/state/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | createReducer,
3 | createSetValueAction,
4 | setValueReducer,
5 | } from '../../common/redux-helper';
6 | import { FetchStatus } from '../constant';
7 |
8 | export const Types = {
9 | SetValue: 'common/SetValue',
10 | SetIsSlow: 'common/SetIsSlow',
11 | SetFetchStatus: 'common/SetFetchStatus',
12 | };
13 |
14 | export const actions = {
15 | setValue: createSetValueAction(Types.SetValue),
16 | setIsSlow: payload => ({ type: Types.SetIsSlow, payload }),
17 | setFetchStatus: payload => ({ type: Types.SetFetchStatus, payload }),
18 | };
19 |
20 | const INITIAL_STATE = {
21 | fetchInfo: {
22 | fetchStatusMap: {},
23 | isSlowMap: {},
24 | totalCountMap: {},
25 | errorMessageMap: {},
26 | nextPageMap: {},
27 | },
28 | };
29 | const reducer = createReducer(INITIAL_STATE, {
30 | [Types.SetValue]: setValueReducer,
31 | [Types.SetFetchStatus]: (state, action) => {
32 | const {
33 | actionType,
34 | fetchKey,
35 | status,
36 | totalCount,
37 | nextPage,
38 | errorMessage,
39 | } = action.payload;
40 | if (!state.fetchInfo.fetchStatusMap[actionType]) {
41 | state.fetchInfo.fetchStatusMap[actionType] = {};
42 | }
43 | state.fetchInfo.fetchStatusMap[actionType][fetchKey] = status;
44 |
45 | if (status !== FetchStatus.Request) {
46 | if (state.fetchInfo.isSlowMap[actionType]) {
47 | state.fetchInfo.isSlowMap[actionType][fetchKey] = false;
48 | }
49 | if (totalCount !== undefined) {
50 | if (!state.fetchInfo.totalCountMap[actionType]) {
51 | state.fetchInfo.totalCountMap[actionType] = {};
52 | }
53 | state.fetchInfo.totalCountMap[actionType][fetchKey] = totalCount;
54 | }
55 | if (nextPage !== undefined) {
56 | if (!state.fetchInfo.nextPageMap[actionType]) {
57 | state.fetchInfo.nextPageMap[actionType] = {};
58 | }
59 | state.fetchInfo.nextPageMap[actionType][fetchKey] = nextPage;
60 | }
61 | if (!state.fetchInfo.errorMessageMap[actionType]) {
62 | state.fetchInfo.errorMessageMap[actionType] = {};
63 | }
64 | if (errorMessage) {
65 | state.fetchInfo.errorMessageMap[actionType][fetchKey] = errorMessage;
66 | }
67 | }
68 | },
69 | [Types.SetIsSlow]: (state, action) => {
70 | const { actionType, fetchKey, isSlow } = action.payload;
71 | if (!state.fetchInfo.isSlowMap[actionType]) {
72 | state.fetchInfo.isSlowMap[actionType] = {};
73 | }
74 | state.fetchInfo.isSlowMap[actionType][fetchKey] = isSlow;
75 | },
76 | });
77 | export default reducer;
78 |
--------------------------------------------------------------------------------
/whois/final/src/common/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, compose, applyMiddleware } from 'redux';
2 | import createSagaMiddleware from 'redux-saga';
3 | import { all } from 'redux-saga/effects';
4 | import searchReducer from '../search/state';
5 | import searchSaga from '../search/state/saga';
6 | import userReducer from '../user/state';
7 | import userSaga from '../user/state/saga';
8 | import commonReducer from '../common/state';
9 | import authReducer from '../auth/state';
10 | import authSaga from '../auth/state/saga';
11 |
12 | const reducer = combineReducers({
13 | common: commonReducer,
14 | search: searchReducer,
15 | user: userReducer,
16 | auth: authReducer,
17 | });
18 | const sagaMiddleware = createSagaMiddleware();
19 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
20 | const store = createStore(
21 | reducer,
22 | composeEnhancers(applyMiddleware(sagaMiddleware)),
23 | );
24 |
25 | function* rootSaga() {
26 | yield all([searchSaga(), userSaga(), authSaga()]);
27 | }
28 | sagaMiddleware.run(rootSaga);
29 |
30 | export default store;
31 |
--------------------------------------------------------------------------------
/whois/final/src/common/util/api.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { API_HOST } from "../constant";
3 | import { message } from "antd";
4 |
5 | /**
6 | *
7 | * @param {object} param
8 | * @param {'get' | 'post' =} param.method
9 | * @param {string} param.url
10 | * @param {object=} param.params
11 | * @param {object=} param.data
12 | * @param {object=} param.totalCount
13 | */
14 | export function callApi({ method = "get", url, params, data }) {
15 | return axios({
16 | url,
17 | method,
18 | baseURL: API_HOST,
19 | params,
20 | data,
21 | withCredentials: true,
22 | })
23 | .then((response) => {
24 | const { resultCode, resultMessage, totalCount } = response.data;
25 | if (resultCode < 0) {
26 | message.error(resultMessage);
27 | }
28 | return {
29 | isSuccess: resultCode === ResultCode.Success,
30 | data: response.data.data,
31 | resultCode,
32 | resultMessage,
33 | totalCount,
34 | };
35 | })
36 | .catch(() => {
37 | return {
38 | isSuccess: false,
39 | };
40 | });
41 | }
42 |
43 | export const ResultCode = {
44 | Success: 0,
45 | };
46 |
--------------------------------------------------------------------------------
/whois/final/src/common/util/fetch.js:
--------------------------------------------------------------------------------
1 | import { put, delay, fork, cancel, select, call } from 'redux-saga/effects';
2 | import lruCache from 'lru-cache';
3 | import { FetchStatus } from '../constant';
4 | import { callApi } from './api';
5 | import { actions } from '../state';
6 | import { FETCH_PAGE, FETCH_KEY } from '../redux-helper';
7 |
8 | function makeCheckSlowSaga(actionType, fetchKey) {
9 | return function* () {
10 | yield delay(500);
11 | yield put(
12 | actions.setIsSlow({
13 | actionType,
14 | fetchKey,
15 | isSlow: true,
16 | }),
17 | );
18 | };
19 | }
20 |
21 | const apiCache = new lruCache({
22 | max: 500,
23 | maxAge: 1000 * 60 * 2,
24 | });
25 |
26 | const SAGA_CALL_TYPE = call(() => {}).type;
27 | function getIsCallEffect(value) {
28 | return value && value.type === SAGA_CALL_TYPE;
29 | }
30 | export function makeFetchSaga({
31 | fetchSaga,
32 | canCache,
33 | getTotalCount = res => res?.totalCount,
34 | }) {
35 | return function* (action) {
36 | const { type: actionType } = action;
37 | const fetchPage = action[FETCH_PAGE];
38 | const fetchKey = getFetchKey(action);
39 | const nextPage = yield select(
40 | state => state.common.fetchInfo.nextPageMap[actionType]?.[fetchKey] || 0,
41 | );
42 | const page = fetchPage !== undefined ? fetchPage : nextPage;
43 | const iterStack = [];
44 | let iter = fetchSaga(action, page);
45 | let res;
46 | let checkSlowTask;
47 | let params;
48 | while (true) {
49 | const { value, done } = iter.next(res);
50 | if (getIsCallEffect(value) && getIsGeneratorFunction(value.payload.fn)) {
51 | iterStack.push(iter);
52 | iter = value.payload.fn(...value.payload.args);
53 | continue;
54 | }
55 | if (getIsCallEffect(value) && value.payload.fn === callApi) {
56 | yield put(
57 | actions.setFetchStatus({
58 | actionType,
59 | fetchKey,
60 | status: FetchStatus.Request,
61 | }),
62 | );
63 | const apiParam = value.payload.args[0];
64 | const cacheKey = getApiCacheKey(actionType, apiParam);
65 | let apiResult =
66 | canCache && apiCache.has(cacheKey)
67 | ? apiCache.get(cacheKey)
68 | : undefined;
69 | const isFromCache = !!apiResult;
70 | if (!isFromCache) {
71 | if (!apiResult) {
72 | checkSlowTask = yield fork(makeCheckSlowSaga(actionType, fetchKey));
73 | apiResult = yield value;
74 | if (checkSlowTask) {
75 | yield cancel(checkSlowTask);
76 | }
77 | }
78 | }
79 | res = apiResult;
80 | if (apiResult) {
81 | const isSuccess = apiResult.isSuccess;
82 | if (isSuccess && canCache && !isFromCache) {
83 | apiCache.set(cacheKey, apiResult);
84 | }
85 | const totalCount = getTotalCount(apiResult);
86 | params = {
87 | actionType,
88 | fetchKey,
89 | status: isSuccess ? FetchStatus.Success : FetchStatus.Fail,
90 | totalCount,
91 | nextPage: isSuccess ? page + 1 : page,
92 | errorMessage: isSuccess ? '' : apiResult.resultMessage,
93 | };
94 | }
95 | } else if (value !== undefined) {
96 | res = yield value;
97 | }
98 | if (done) {
99 | const nextIter = iterStack.pop();
100 | if (nextIter) {
101 | iter = nextIter;
102 | continue;
103 | }
104 |
105 | if (params) {
106 | yield put(actions.setFetchStatus(params));
107 | }
108 | break;
109 | }
110 | }
111 | };
112 | }
113 |
114 | // 쿼리 파라미터 순서가 바뀌어도 같은 key가 나오도록 키 이름으로 정렬한다
115 | export function getApiCacheKey(actionType, { apiHost, url, params }) {
116 | const prefix = `${actionType}_${apiHost ? apiHost + url : url}`;
117 | const keys = params ? Object.keys(params) : [];
118 | if (keys.length) {
119 | return (
120 | prefix +
121 | keys.sort().reduce((acc, key) => `${acc}&${key}=${params[key]}`, '')
122 | );
123 | } else {
124 | return prefix;
125 | }
126 | }
127 |
128 | export function getFetchKey(action) {
129 | const fetchKey = action[FETCH_KEY];
130 | return fetchKey === undefined ? action.type : String(fetchKey);
131 | }
132 |
133 | function getIsGeneratorFunction(obj) {
134 | const constructor = obj.constructor;
135 | if (!constructor) {
136 | return false;
137 | }
138 | if (
139 | 'GeneratorFunction' === constructor.name ||
140 | 'GeneratorFunction' === constructor.displayName
141 | ) {
142 | return true;
143 | }
144 | const proto = constructor.prototype;
145 | return 'function' === typeof proto.next && 'function' === typeof proto.throw;
146 | }
147 |
148 | /**
149 | *
150 | * @param {string=} actionType
151 | */
152 | export function deleteApiCache(actionType) {
153 | let keys = apiCache.keys();
154 | if (actionType) {
155 | keys = keys.filter(key => key.includes(actionType));
156 | }
157 | for (const key of keys) {
158 | apiCache.del(key);
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/whois/final/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import store from './common/store';
5 | import { Provider } from 'react-redux';
6 | import { BrowserRouter } from 'react-router-dom';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 | ,
14 | document.getElementById('root'),
15 | );
16 |
--------------------------------------------------------------------------------
/whois/final/src/search/component/Settings.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dropdown, Menu, Button } from 'antd';
3 | import { SettingFilled } from '@ant-design/icons';
4 |
5 | /**
6 | *
7 | * @param {object} param
8 | * @param {() => void} param.logout
9 | */
10 | export default function Settings({ logout }) {
11 | return (
12 |
15 | 로그아웃
16 |
17 | }
18 | trigger={['click']}
19 | placement="bottomRight"
20 | >
21 | } />
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/whois/final/src/search/container/Search.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Row, Col, Typography } from 'antd';
3 | import Settings from '../component/Settings';
4 | import SearchInput from '../container/SearchInput';
5 | import History from '../../common/component/History';
6 | import { useSelector, useDispatch } from 'react-redux';
7 | import { actions } from '../state';
8 | import useNeedLogin from '../../common/hook/useNeedLogin';
9 | import { actions as authActions } from '../../auth/state';
10 |
11 | export default function Search() {
12 | useNeedLogin();
13 | const history = useSelector(state => state.search.history);
14 | const dispatch = useDispatch();
15 |
16 | useEffect(() => {
17 | dispatch(actions.fetchAllHistory());
18 | }, [dispatch]);
19 |
20 | function logout() {
21 | dispatch(authActions.fetchLogout());
22 | }
23 |
24 | return (
25 | <>
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 찾 아 야 한 다
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | >
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/whois/final/src/search/container/SearchInput.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { AutoComplete, Input, Space, Typography } from 'antd';
3 | import { SearchOutlined } from '@ant-design/icons';
4 | import { useSelector, useDispatch } from 'react-redux';
5 | import { actions } from '../state';
6 | import { actions as userActions } from '../../user/state';
7 | import { useHistory } from 'react-router-dom';
8 |
9 | export default function Search() {
10 | const keyword = useSelector(state => state.search.keyword);
11 | const dispatch = useDispatch();
12 | function setKeyword(value) {
13 | if (value !== keyword) {
14 | dispatch(actions.setValue('keyword', value));
15 | dispatch(actions.fetchAutoComplete(value));
16 | }
17 | }
18 |
19 | const autoCompletes = useSelector(state => state.search.autoCompletes);
20 | const history = useHistory();
21 | function goToUser(value) {
22 | const user = autoCompletes.find(item => item.name === value);
23 | if (user) {
24 | dispatch(userActions.setValue('user', user));
25 | history.push(`/user/${user.name}`);
26 | }
27 | }
28 |
29 | return (
30 | ({
36 | value: item.name,
37 | label: (
38 |
39 | {item.name}
40 |
41 | {item.department}
42 |
43 | {item.tag}
44 |
45 | ),
46 | }))}
47 | autoFocus
48 | >
49 | }
53 | />
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/whois/final/src/search/state/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | createReducer,
3 | createSetValueAction,
4 | setValueReducer,
5 | } from '../../common/redux-helper';
6 |
7 | export const Types = {
8 | SetValue: 'search/SetValue',
9 | FetchAutoComplete: 'search/FetchAutoComplete',
10 | FetchAllHistory: 'search/FetchAllHistory',
11 | };
12 |
13 | export const actions = {
14 | setValue: createSetValueAction(Types.SetValue),
15 | fetchAutoComplete: keyword => ({
16 | type: Types.FetchAutoComplete,
17 | keyword,
18 | }),
19 | fetchAllHistory: () => ({ type: Types.FetchAllHistory }),
20 | };
21 |
22 | const INITIAL_STATE = {
23 | keyword: '',
24 | autoCompletes: [],
25 | history: [],
26 | };
27 | const reducer = createReducer(INITIAL_STATE, {
28 | [Types.SetValue]: setValueReducer,
29 | });
30 | export default reducer;
31 |
--------------------------------------------------------------------------------
/whois/final/src/search/state/saga.js:
--------------------------------------------------------------------------------
1 | import { all, put, call, takeEvery, takeLeading } from 'redux-saga/effects';
2 | import { actions, Types } from './index';
3 | import { callApi } from '../../common/util/api';
4 | import { makeFetchSaga } from '../../common/util/fetch';
5 |
6 | function* fetchAutoComplete({ keyword }) {
7 | const { isSuccess, data } = yield call(callApi, {
8 | url: '/user/search',
9 | params: { keyword },
10 | });
11 |
12 | if (isSuccess && data) {
13 | yield put(actions.setValue('autoCompletes', data));
14 | }
15 | }
16 |
17 | function* fetchAllHistory() {
18 | const { isSuccess, data } = yield call(callApi, {
19 | url: '/history',
20 | });
21 |
22 | if (isSuccess && data) {
23 | yield put(actions.setValue('history', data));
24 | }
25 | }
26 |
27 | export default function* () {
28 | yield all([
29 | takeEvery(
30 | Types.FetchAutoComplete,
31 | makeFetchSaga({ fetchSaga: fetchAutoComplete, canCache: true }),
32 | ),
33 | takeLeading(
34 | Types.FetchAllHistory,
35 | makeFetchSaga({ fetchSaga: fetchAllHistory, canCache: false }),
36 | ),
37 | ]);
38 | }
39 |
--------------------------------------------------------------------------------
/whois/final/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/whois/final/src/user/component/FetchLabel.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Space, Spin } from 'antd';
3 | import useFetchInfo from '../../common/hook/useFetchInfo';
4 |
5 | /**
6 | *
7 | * @param {object} param
8 | * @param {string} param.label
9 | * @param {string} param.actionType
10 | * @param {string=} param.fetchKey
11 | */
12 | export default function FetchLabel({ label, actionType, fetchKey }) {
13 | const { isSlow } = useFetchInfo(actionType, fetchKey);
14 | return (
15 |
16 | {label}
17 | {isSlow && }
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/whois/final/src/user/container/Department.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Input, message } from 'antd';
3 | import { useSelector, useDispatch } from 'react-redux';
4 | import { actions } from '../state';
5 |
6 | export default function Department() {
7 | const [isEditDepartment, setIsEditDepartment] = useState(false);
8 | const [tempDepartment, setTempDepartment] = useState('');
9 | const user = useSelector(state => state.user.user);
10 | const dispatch = useDispatch();
11 |
12 | function onSaveDepartment() {
13 | if (tempDepartment) {
14 | dispatch(
15 | actions.fetchUpdateUser({
16 | user,
17 | key: 'department',
18 | value: tempDepartment,
19 | fetchKey: 'department',
20 | }),
21 | );
22 | setIsEditDepartment(false);
23 | } else {
24 | message.error('소속은 필수 값입니다.');
25 | }
26 | }
27 |
28 | function onEditDepartment() {
29 | setIsEditDepartment(true);
30 | setTempDepartment(user.department);
31 | }
32 |
33 | return (
34 | <>
35 | {isEditDepartment && (
36 | setTempDepartment(e.target.value)}
40 | onPressEnter={onSaveDepartment}
41 | onBlur={() => setIsEditDepartment(false)}
42 | style={{ width: '100%' }}
43 | />
44 | )}
45 | {!isEditDepartment && (
46 |
54 | )}
55 | >
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/whois/final/src/user/container/TagList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { Tag, Input, message } from 'antd';
4 | import { actions } from '../state';
5 | import { PlusOutlined } from '@ant-design/icons';
6 |
7 | export default function TagList() {
8 | const dispatch = useDispatch();
9 | const user = useSelector(state => state.user.user);
10 | const tags = user?.tag ? user.tag.split(',').map(item => item.trim()) : [];
11 |
12 | const [isAdd, setIsAdd] = useState(false);
13 | const [tempTag, setTempTag] = useState('');
14 | function onAdd() {
15 | setIsAdd(true);
16 | setTempTag('');
17 | }
18 |
19 | function onSave() {
20 | if (!tempTag) {
21 | setIsAdd(false);
22 | } else if (tags.includes(tempTag)) {
23 | message.error('이미 같은 태그가 있습니다.');
24 | } else {
25 | const newTag = user?.tag ? `${user.tag}, ${tempTag}` : tempTag;
26 | dispatch(
27 | actions.fetchUpdateUser({
28 | user,
29 | key: 'tag',
30 | value: newTag,
31 | fetchKey: 'tag',
32 | }),
33 | );
34 | setIsAdd(false);
35 | }
36 | }
37 |
38 | function onDelete(tag) {
39 | const newTag = tags.filter(item => item !== tag).join(', ');
40 | dispatch(
41 | actions.fetchUpdateUser({
42 | user,
43 | key: 'tag',
44 | value: newTag,
45 | fetchKey: 'tag',
46 | }),
47 | );
48 | }
49 |
50 | return (
51 | <>
52 | {tags.map(item => (
53 | onDelete(item)}>
54 | {item}
55 |
56 | ))}
57 | {!isAdd && (
58 |
59 | New Tag
60 |
61 | )}
62 | {isAdd && (
63 | setTempTag(e.target.value)}
70 | onBlur={() => setIsAdd(false)}
71 | onPressEnter={onSave}
72 | />
73 | )}
74 | >
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/whois/final/src/user/container/User.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { PageHeader, Col, Row, Descriptions, Typography } from 'antd';
3 | import { useHistory } from 'react-router-dom';
4 | import { useSelector, useDispatch } from 'react-redux';
5 | import { actions, Types } from '../state';
6 | import useFetchInfo from '../../common/hook/useFetchInfo';
7 | import History from '../../common/component/History';
8 | import Department from './Department';
9 | import TagList from './TagList';
10 | import FetchLabel from '../component/FetchLabel';
11 | import useNeedLogin from '../../common/hook/useNeedLogin';
12 |
13 | /**
14 | *
15 | * @param {object} param
16 | * @param {import('react-router').match} param.match
17 | */
18 | export default function User({ match }) {
19 | useNeedLogin();
20 | const history = useHistory();
21 | const dispatch = useDispatch();
22 | const user = useSelector(state => state.user.user);
23 | const userHistory = useSelector(state => state.user.userHistory);
24 |
25 | const name = match.params.name;
26 | useEffect(() => {
27 | dispatch(actions.fetchUser(name));
28 | dispatch(actions.fetchUserHistory(name));
29 | }, [dispatch, name]);
30 |
31 | useEffect(() => {
32 | return () => dispatch(actions.initialize());
33 | }, [dispatch]);
34 |
35 | const { isFetched } = useFetchInfo(Types.FetchUser);
36 |
37 | return (
38 |
39 |
40 | history.push('/')}
42 | title={
43 |
44 | }
45 | >
46 | {user && (
47 |
48 |
49 | {user.name}
50 |
51 |
58 | }
59 | >
60 |
61 |
62 |
69 | }
70 | >
71 |
72 |
73 |
74 |
75 |
76 |
77 | )}
78 | {!user && isFetched && (
79 | 존재하지 않는 사용자 입니다.
80 | )}
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/whois/final/src/user/state/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | createReducer,
3 | createSetValueAction,
4 | setValueReducer,
5 | FETCH_KEY,
6 | NOT_IMMUTABLE,
7 | } from '../../common/redux-helper';
8 |
9 | export const Types = {
10 | SetValue: 'user/SetValue',
11 | FetchUser: 'user/FetchUser',
12 | FetchUpdateUser: 'user/FetchUpdateUser',
13 | FetchUserHistory: 'user/FetchUserHistory',
14 | AddHistory: 'user/AddHistory',
15 | Initialize: 'user/Initialize',
16 | };
17 |
18 | export const actions = {
19 | setValue: createSetValueAction(Types.SetValue),
20 | fetchUser: name => ({ type: Types.FetchUser, name }),
21 | fetchUpdateUser: ({ user, key, value, fetchKey }) => ({
22 | type: Types.FetchUpdateUser,
23 | user,
24 | key,
25 | value,
26 | [FETCH_KEY]: fetchKey,
27 | }),
28 | fetchUserHistory: name => ({ type: Types.FetchUserHistory, name }),
29 | addHistory: history => ({ type: Types.AddHistory, history }),
30 | initialize: () => ({ type: Types.Initialize, [NOT_IMMUTABLE]: true }),
31 | };
32 |
33 | const INITIAL_STATE = {
34 | user: undefined,
35 | userHistory: [],
36 | };
37 | const reducer = createReducer(INITIAL_STATE, {
38 | [Types.SetValue]: setValueReducer,
39 | [Types.AddHistory]: (state, action) =>
40 | (state.userHistory = [action.history, ...state.userHistory]),
41 | [Types.Initialize]: () => INITIAL_STATE,
42 | });
43 | export default reducer;
44 |
--------------------------------------------------------------------------------
/whois/final/src/user/state/saga.js:
--------------------------------------------------------------------------------
1 | import { all, call, put, takeLeading } from 'redux-saga/effects';
2 | import { Types, actions } from '.';
3 | import { callApi } from '../../common/util/api';
4 | import { makeFetchSaga, deleteApiCache } from '../../common/util/fetch';
5 |
6 | function* fetchUser({ name }) {
7 | const { isSuccess, data } = yield call(callApi, {
8 | url: '/user/search',
9 | params: { keyword: name },
10 | });
11 |
12 | if (isSuccess && data) {
13 | const user = data.find(item => item.name === name);
14 | if (user) {
15 | yield put(actions.setValue('user', user));
16 | }
17 | }
18 | }
19 |
20 | function* fetchUpdateUser({ user, key, value }) {
21 | const oldValue = user[key];
22 | yield put(actions.setValue('user', { ...user, [key]: value }));
23 | const { isSuccess, data } = yield call(callApi, {
24 | url: '/user/update',
25 | method: 'post',
26 | data: { name: user.name, key, value, oldValue },
27 | });
28 |
29 | if (isSuccess && data) {
30 | deleteApiCache();
31 | yield put(actions.addHistory(data.history));
32 | } else {
33 | yield put(actions.setValue('user', user));
34 | }
35 | }
36 |
37 | function* fetchUserHistory({ name }) {
38 | const { isSuccess, data } = yield call(callApi, {
39 | url: '/history',
40 | params: { name },
41 | });
42 |
43 | if (isSuccess && data) {
44 | yield put(actions.setValue('userHistory', data));
45 | }
46 | }
47 |
48 | export default function* () {
49 | yield all([
50 | takeLeading(
51 | Types.FetchUser,
52 | makeFetchSaga({ fetchSaga: fetchUser, canCache: false }),
53 | ),
54 | takeLeading(
55 | Types.FetchUpdateUser,
56 | makeFetchSaga({ fetchSaga: fetchUpdateUser, canCache: false }),
57 | ),
58 | takeLeading(
59 | Types.FetchUserHistory,
60 | makeFetchSaga({ fetchSaga: fetchUserHistory, canCache: false }),
61 | ),
62 | ]);
63 | }
64 |
--------------------------------------------------------------------------------
/whois/start/.env.development:
--------------------------------------------------------------------------------
1 | REACT_APP_API_HOST=http://localhost:3001
--------------------------------------------------------------------------------
/whois/start/.env.production:
--------------------------------------------------------------------------------
1 | REACT_APP_API_HOST=http://localhost:3001
--------------------------------------------------------------------------------
/whois/start/.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 |
--------------------------------------------------------------------------------
/whois/start/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "module": "commonjs",
5 | "target": "es2020",
6 | "checkJs": true
7 | },
8 | "exclude": ["node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/whois/start/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "whois",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.5.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "react": "^16.13.1",
10 | "react-dom": "^16.13.1",
11 | "react-scripts": "3.4.1",
12 | "@ant-design/icons": "^4.2.1",
13 | "@testing-library/react-hooks": "^3.3.0",
14 | "@types/jest": "^26.0.4",
15 | "antd": "^4.4.2",
16 | "axios": "^0.19.2",
17 | "diff": "^4.0.2",
18 | "react-redux": "^7.2.0",
19 | "react-router-dom": "^5.2.0",
20 | "react-test-renderer": "^16.13.1",
21 | "redux": "^4.0.5",
22 | "redux-saga": "^1.1.3"
23 | },
24 | "scripts": {
25 | "start": "react-scripts start",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test",
28 | "eject": "react-scripts eject"
29 | },
30 | "eslintConfig": {
31 | "extends": "react-app"
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/whois/start/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/whois/start/public/favicon.ico
--------------------------------------------------------------------------------
/whois/start/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
37 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/whois/start/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/whois/start/public/logo192.png
--------------------------------------------------------------------------------
/whois/start/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/whois/start/public/logo512.png
--------------------------------------------------------------------------------
/whois/start/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/whois/start/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/whois/start/server/data.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landvibe/inflearn-react-project/0cf5de7764649b64c1626b991c3d5f07d6b058cb/whois/start/server/data.db
--------------------------------------------------------------------------------
/whois/start/server/db.js:
--------------------------------------------------------------------------------
1 | const sqlite3 = require('sqlite3');
2 |
3 | // const db = new sqlite3.Database(':memory:');
4 | const db = new sqlite3.Database('./data.db', sqlite3.OPEN_READWRITE);
5 |
6 | const users = [
7 | ['land', '글로벌웹', '팀장, 웹, 결제, 리액트'],
8 | ['bono', '글로벌웹', '팀원, 로그인, 작품홈'],
9 | ['shai', '국내웹', '팀장, 비디오 플레이어, 카톡더보기'],
10 | ];
11 | const placeholders = users.map(_ => '(?,?,?)').join(',');
12 | const sql = 'INSERT INTO user(name, department, tag) VALUES ' + placeholders;
13 | db.run(
14 | sql,
15 | users.flatMap(_ => _),
16 | function (err) {
17 | if (err) {
18 | return console.error(err.message);
19 | }
20 | console.log(`Rows inserted ${this.changes}`);
21 | },
22 | );
23 |
24 | db.close();
25 |
--------------------------------------------------------------------------------
/whois/start/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const sqlite3 = require('sqlite3');
3 | const cors = require('cors');
4 | const bodyParser = require('body-parser');
5 | const cookieParser = require('cookie-parser');
6 |
7 | const app = express();
8 | app.use(
9 | cors({
10 | origin: 'http://localhost:3000',
11 | credentials: true,
12 | }),
13 | );
14 | app.use(bodyParser.json());
15 | app.use(cookieParser());
16 |
17 | const db = new sqlite3.Database('./data.db', sqlite3.OPEN_READWRITE);
18 | app.get('/user/search', (req, res) => {
19 | setTimeout(() => {
20 | const keyword = req.query.keyword;
21 | db.all(
22 | `SELECT * FROM user where name like '%${keyword}%' or department like '%${keyword}%' or tag like '%${keyword}%'`,
23 | [],
24 | (err, rows) => {
25 | if (err) {
26 | throw err;
27 | }
28 | res.send(makeResponse({ data: rows }));
29 | },
30 | );
31 | }, 1);
32 | });
33 | app.get('/history', (req, res) => {
34 | setTimeout(() => {
35 | const { name, page = 0 } = req.query;
36 | // @ts-ignore
37 | const pagination = `limit ${PAGING_SIZE} offset ${PAGING_SIZE * page}`;
38 | const sql = name
39 | ? `SELECT * FROM history where name='${name}' order by date DESC ${pagination}`
40 | : `SELECT * FROM history order by date DESC ${pagination}`;
41 | db.all(sql, [], (err, rows) => {
42 | if (err) {
43 | throw err;
44 | }
45 | db.all('SELECT count(*) as totalCount FROM history', [], (err, rows2) => {
46 | const totalCount = rows2[0].totalCount;
47 | res.send(makeResponse({ data: rows, totalCount }));
48 | });
49 | });
50 | }, 1);
51 | });
52 | app.post('/user/update', (req, res) => {
53 | setTimeout(() => {
54 | const { key, name, value, oldValue } = req.body;
55 | const sql = `UPDATE user SET ${key} = ? WHERE name = ?`;
56 | db.run(sql, [value, name], function (err) {
57 | if (err) {
58 | return console.error(err.message);
59 | }
60 |
61 | const date = new Date(new Date().getTime() + 9 * 3600 * 1000);
62 | const iso = date.toISOString();
63 | const dateStr = `${iso.substr(0, 10)} ${iso.substr(11, 8)}`;
64 | const editor = req.cookies.token || 'unknown';
65 | const history = {
66 | editor,
67 | name,
68 | column: key,
69 | before: oldValue,
70 | after: value,
71 | date: dateStr,
72 | };
73 | const sql = `INSERT INTO history(editor, name, column, before, after, date) VALUES (?,?,?,?,?,?)`;
74 | db.run(
75 | sql,
76 | [
77 | history.editor,
78 | history.name,
79 | history.column,
80 | history.before,
81 | history.after,
82 | history.date,
83 | ],
84 | function (err) {
85 | if (err) {
86 | return console.error(err.message);
87 | }
88 | history.id = this.lastID;
89 | res.send(makeResponse({ data: { history } }));
90 | },
91 | );
92 | });
93 | }, 1);
94 | });
95 |
96 | app.get('/auth/user', (req, res) => {
97 | setTimeout(() => {
98 | const name = req.cookies.token;
99 | res.send(makeResponse({ data: { name } }));
100 | }, 1);
101 | });
102 |
103 | app.post('/auth/login', (req, res) => {
104 | setTimeout(() => {
105 | const { name } = req.body;
106 | db.all(`SELECT * FROM user where name='${name}'`, [], (err, rows) => {
107 | if (err) {
108 | throw err;
109 | }
110 | if (rows.length) {
111 | res.cookie('token', name, {
112 | maxAge: COOKIE_MAX_AGE,
113 | httpOnly: true,
114 | });
115 | res.send(makeResponse({ data: { name } }));
116 | } else {
117 | res.send(
118 | makeResponse({
119 | resultCode: -1,
120 | resultMessage: '존재하지 않는 사용자입니다.',
121 | }),
122 | );
123 | }
124 | });
125 | }, 1);
126 | });
127 |
128 | app.get('/auth/logout', (req, res) => {
129 | setTimeout(() => {
130 | res.cookie('token', '', {
131 | maxAge: 0,
132 | httpOnly: true,
133 | });
134 | res.send(makeResponse({}));
135 | }, 1);
136 | });
137 |
138 | app.post('/auth/signup', (req, res) => {
139 | setTimeout(() => {
140 | const { email } = req.body;
141 | if (!email.includes('@')) {
142 | res.send(
143 | makeResponse({
144 | resultCode: -1,
145 | resultMessage: '이메일 형식이 아닙니다.',
146 | }),
147 | );
148 | return;
149 | }
150 | const name = email.substr(0, email.lastIndexOf('@'));
151 | db.all(`SELECT * FROM user where name='${name}'`, [], (err, rows) => {
152 | if (err) {
153 | throw err;
154 | }
155 | console.log('rows', rows, rows[0]);
156 | if (rows.length) {
157 | res.send(
158 | makeResponse({
159 | resultCode: -1,
160 | resultMessage: '이미 존재하는 사용자입니다.',
161 | }),
162 | );
163 | } else {
164 | const sql = `INSERT INTO user(name, department, tag) VALUES (?,?,?)`;
165 | db.run(sql, [name, '소속없음', ''], function (err) {
166 | if (err) {
167 | return console.error(err.message);
168 | }
169 | res.cookie('token', name, { maxAge: COOKIE_MAX_AGE, httpOnly: true });
170 | res.send(makeResponse({ data: { name } }));
171 | });
172 | }
173 | });
174 | }, 1);
175 | });
176 |
177 | const COOKIE_MAX_AGE = 3600000 * 24 * 14;
178 | const PAGING_SIZE = 20;
179 |
180 | /**
181 | *
182 | * @param {object} param
183 | * @param {object=} param.data
184 | * @param {number=} param.totalCount
185 | * @param {number=} param.resultCode
186 | * @param {string=} param.resultMessage
187 | */
188 | function makeResponse({ data, totalCount, resultCode, resultMessage }) {
189 | return {
190 | data,
191 | totalCount,
192 | resultCode: resultCode || 0,
193 | resultMessage: resultMessage || '',
194 | };
195 | }
196 |
197 | const PORT = 3001;
198 | app.listen(PORT, () => console.log(`app listening on port ${PORT}!`));
199 |
--------------------------------------------------------------------------------
/whois/start/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "nodemon index.js"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "body-parser": "^1.19.0",
14 | "cookie-parser": "^1.4.5",
15 | "cors": "^2.8.5",
16 | "express": "^4.17.1",
17 | "lru-cache": "^6.0.0",
18 | "nodemon": "^2.0.4",
19 | "sqlite3": "^5.1.4"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/whois/start/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function App() {
4 | return 찾아야한다
;
5 | }
6 |
--------------------------------------------------------------------------------
/whois/start/src/common/constant.js:
--------------------------------------------------------------------------------
1 | export const API_HOST = process.env.REACT_APP_API_HOST;
2 | export const FetchStatus = {
3 | Request: 'Request',
4 | Success: 'Success',
5 | Fail: 'Fail',
6 | };
7 | export const AuthStatus = {
8 | Login: 'Login',
9 | NotLogin: 'NotLogin',
10 | };
11 |
--------------------------------------------------------------------------------
/whois/start/src/common/hook/useFetchInfo.js:
--------------------------------------------------------------------------------
1 | import { getFetchKey } from '../util/fetch';
2 | import { useSelector, shallowEqual } from 'react-redux';
3 | import { FetchStatus } from '../constant';
4 | import { FETCH_KEY } from '../redux-helper';
5 |
6 | export default function useFetchInfo(actionType, fetchKey) {
7 | const _fetchKey = getFetchKey({
8 | type: actionType,
9 | [FETCH_KEY]: fetchKey,
10 | });
11 | return useSelector(
12 | state => ({
13 | fetchStatus:
14 | state.common.fetchInfo.fetchStatusMap[actionType]?.[_fetchKey],
15 | isFetching:
16 | state.common.fetchInfo.fetchStatusMap[actionType]?.[_fetchKey] ===
17 | FetchStatus.Request,
18 | isFetched:
19 | state.common.fetchInfo.fetchStatusMap[actionType]?.[_fetchKey] ===
20 | FetchStatus.Success ||
21 | state.common.fetchInfo.fetchStatusMap[actionType]?.[_fetchKey] ===
22 | FetchStatus.Fail,
23 | isSlow: !!state.common.fetchInfo.isSlowMap[actionType]?.[_fetchKey],
24 | nextPage:
25 | state.common.fetchInfo.nextPageMap[actionType]?.[_fetchKey] || 0,
26 | totalCount:
27 | state.common.fetchInfo.totalCountMap[actionType]?.[_fetchKey] || 0,
28 | errorMessage:
29 | state.common.fetchInfo.errorMessageMap[actionType]?.[_fetchKey],
30 | }),
31 | shallowEqual,
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/whois/start/src/common/redux-helper.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | export function createReducer(initialState, handlerMap) {
4 | return function (state = initialState, action) {
5 | const handler = handlerMap[action.type];
6 | if (handler) {
7 | if (action[NOT_IMMUTABLE]) {
8 | return handler(state, action);
9 | } else {
10 | return produce(state, draft => {
11 | const handler = handlerMap[action.type];
12 | handler(draft, action);
13 | });
14 | }
15 | } else {
16 | return state;
17 | }
18 | };
19 | }
20 |
21 | export function createSetValueAction(type) {
22 | return (key, value) => ({ type, key, value });
23 | }
24 | export function setValueReducer(state, action) {
25 | state[action.key] = action.value;
26 | }
27 |
28 | export const FETCH_PAGE = Symbol('FETCH_PAGE');
29 | export const FETCH_KEY = Symbol('FETCH_KEY');
30 | export const NOT_IMMUTABLE = Symbol('NOT_IMMUTABLE');
31 |
--------------------------------------------------------------------------------
/whois/start/src/common/state/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | createReducer,
3 | createSetValueAction,
4 | setValueReducer,
5 | } from '../../common/redux-helper';
6 | import { FetchStatus } from '../constant';
7 |
8 | export const Types = {
9 | SetValue: 'common/SetValue',
10 | SetIsSlow: 'common/SetIsSlow',
11 | SetFetchStatus: 'common/SetFetchStatus',
12 | };
13 |
14 | export const actions = {
15 | setValue: createSetValueAction(Types.SetValue),
16 | setIsSlow: payload => ({ type: Types.SetIsSlow, payload }),
17 | setFetchStatus: payload => ({ type: Types.SetFetchStatus, payload }),
18 | };
19 |
20 | const INITIAL_STATE = {
21 | fetchInfo: {
22 | fetchStatusMap: {},
23 | isSlowMap: {},
24 | totalCountMap: {},
25 | errorMessageMap: {},
26 | nextPageMap: {},
27 | },
28 | };
29 | const reducer = createReducer(INITIAL_STATE, {
30 | [Types.SetValue]: setValueReducer,
31 | [Types.SetFetchStatus]: (state, action) => {
32 | const {
33 | actionType,
34 | fetchKey,
35 | status,
36 | totalCount,
37 | nextPage,
38 | errorMessage,
39 | } = action.payload;
40 | if (!state.fetchInfo.fetchStatusMap[actionType]) {
41 | state.fetchInfo.fetchStatusMap[actionType] = {};
42 | }
43 | state.fetchInfo.fetchStatusMap[actionType][fetchKey] = status;
44 |
45 | if (status !== FetchStatus.Request) {
46 | if (state.fetchInfo.isSlowMap[actionType]) {
47 | state.fetchInfo.isSlowMap[actionType][fetchKey] = false;
48 | }
49 | if (totalCount !== undefined) {
50 | if (!state.fetchInfo.totalCountMap[actionType]) {
51 | state.fetchInfo.totalCountMap[actionType] = {};
52 | }
53 | state.fetchInfo.totalCountMap[actionType][fetchKey] = totalCount;
54 | }
55 | if (nextPage !== undefined) {
56 | if (!state.fetchInfo.nextPageMap[actionType]) {
57 | state.fetchInfo.nextPageMap[actionType] = {};
58 | }
59 | state.fetchInfo.nextPageMap[actionType][fetchKey] = nextPage;
60 | }
61 | if (!state.fetchInfo.errorMessageMap[actionType]) {
62 | state.fetchInfo.errorMessageMap[actionType] = {};
63 | }
64 | if (errorMessage) {
65 | state.fetchInfo.errorMessageMap[actionType][fetchKey] = errorMessage;
66 | }
67 | }
68 | },
69 | [Types.SetIsSlow]: (state, action) => {
70 | const { actionType, fetchKey, isSlow } = action.payload;
71 | if (!state.fetchInfo.isSlowMap[actionType]) {
72 | state.fetchInfo.isSlowMap[actionType] = {};
73 | }
74 | state.fetchInfo.isSlowMap[actionType][fetchKey] = isSlow;
75 | },
76 | });
77 | export default reducer;
78 |
--------------------------------------------------------------------------------
/whois/start/src/common/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, compose, applyMiddleware } from 'redux';
2 | import createSagaMiddleware from 'redux-saga';
3 | import { all } from 'redux-saga/effects';
4 | import commonReducer from '../common/state';
5 |
6 | const reducer = combineReducers({
7 | common: commonReducer,
8 | });
9 | const sagaMiddleware = createSagaMiddleware();
10 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
11 | const store = createStore(
12 | reducer,
13 | composeEnhancers(applyMiddleware(sagaMiddleware)),
14 | );
15 |
16 | function* rootSaga() {
17 | yield all([]);
18 | }
19 | sagaMiddleware.run(rootSaga);
20 |
21 | export default store;
22 |
--------------------------------------------------------------------------------
/whois/start/src/common/util/api.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { API_HOST } from "../constant";
3 | import { message } from "antd";
4 |
5 | /**
6 | *
7 | * @param {object} param
8 | * @param {'get' | 'post' =} param.method
9 | * @param {string} param.url
10 | * @param {object=} param.params
11 | * @param {object=} param.data
12 | * @param {object=} param.totalCount
13 | */
14 | export function callApi({ method = "get", url, params, data }) {
15 | return axios({
16 | url,
17 | method,
18 | baseURL: API_HOST,
19 | params,
20 | data,
21 | withCredentials: true,
22 | })
23 | .then((response) => {
24 | const { resultCode, resultMessage, totalCount } = response.data;
25 | if (resultCode < 0) {
26 | message.error(resultMessage);
27 | }
28 | return {
29 | isSuccess: resultCode === ResultCode.Success,
30 | data: response.data.data,
31 | resultCode,
32 | resultMessage,
33 | totalCount,
34 | };
35 | })
36 | .catch(() => {
37 | return {
38 | isSuccess: false,
39 | };
40 | });
41 | }
42 |
43 | export const ResultCode = {
44 | Success: 0,
45 | };
46 |
--------------------------------------------------------------------------------
/whois/start/src/common/util/fetch.js:
--------------------------------------------------------------------------------
1 | import { put, delay, fork, cancel, select, call } from 'redux-saga/effects';
2 | import lruCache from 'lru-cache';
3 | import { FetchStatus } from '../constant';
4 | import { callApi } from './api';
5 | import { actions } from '../state';
6 | import { FETCH_PAGE, FETCH_KEY } from '../redux-helper';
7 |
8 | function makeCheckSlowSaga(actionType, fetchKey) {
9 | return function* () {
10 | yield delay(500);
11 | yield put(
12 | actions.setIsSlow({
13 | actionType,
14 | fetchKey,
15 | isSlow: true,
16 | }),
17 | );
18 | };
19 | }
20 |
21 | const apiCache = new lruCache({
22 | max: 500,
23 | maxAge: 1000 * 60 * 2,
24 | });
25 |
26 | const SAGA_CALL_TYPE = call(() => {}).type;
27 | function getIsCallEffect(value) {
28 | return value && value.type === SAGA_CALL_TYPE;
29 | }
30 | export function makeFetchSaga({
31 | fetchSaga,
32 | canCache,
33 | getTotalCount = res => res?.totalCount,
34 | }) {
35 | return function* (action) {
36 | const { type: actionType } = action;
37 | const fetchPage = action[FETCH_PAGE];
38 | const fetchKey = getFetchKey(action);
39 | const nextPage = yield select(
40 | state => state.common.fetchInfo.nextPageMap[actionType]?.[fetchKey] || 0,
41 | );
42 | const page = fetchPage !== undefined ? fetchPage : nextPage;
43 | const iterStack = [];
44 | let iter = fetchSaga(action, page);
45 | let res;
46 | let checkSlowTask;
47 | let params;
48 | while (true) {
49 | const { value, done } = iter.next(res);
50 | if (getIsCallEffect(value) && getIsGeneratorFunction(value.payload.fn)) {
51 | iterStack.push(iter);
52 | iter = value.payload.fn(...value.payload.args);
53 | continue;
54 | }
55 | if (getIsCallEffect(value) && value.payload.fn === callApi) {
56 | yield put(
57 | actions.setFetchStatus({
58 | actionType,
59 | fetchKey,
60 | status: FetchStatus.Request,
61 | }),
62 | );
63 | const apiParam = value.payload.args[0];
64 | const cacheKey = getApiCacheKey(actionType, apiParam);
65 | let apiResult =
66 | canCache && apiCache.has(cacheKey)
67 | ? apiCache.get(cacheKey)
68 | : undefined;
69 | const isFromCache = !!apiResult;
70 | if (!isFromCache) {
71 | if (!apiResult) {
72 | checkSlowTask = yield fork(makeCheckSlowSaga(actionType, fetchKey));
73 | apiResult = yield value;
74 | if (checkSlowTask) {
75 | yield cancel(checkSlowTask);
76 | }
77 | }
78 | }
79 | res = apiResult;
80 | if (apiResult) {
81 | const isSuccess = apiResult.isSuccess;
82 | if (isSuccess && canCache && !isFromCache) {
83 | apiCache.set(cacheKey, apiResult);
84 | }
85 | const totalCount = getTotalCount(apiResult);
86 | params = {
87 | actionType,
88 | fetchKey,
89 | status: isSuccess ? FetchStatus.Success : FetchStatus.Fail,
90 | totalCount,
91 | nextPage: isSuccess ? page + 1 : page,
92 | errorMessage: isSuccess ? '' : apiResult.resultMessage,
93 | };
94 | }
95 | } else if (value !== undefined) {
96 | res = yield value;
97 | }
98 | if (done) {
99 | const nextIter = iterStack.pop();
100 | if (nextIter) {
101 | iter = nextIter;
102 | continue;
103 | }
104 |
105 | if (params) {
106 | yield put(actions.setFetchStatus(params));
107 | }
108 | break;
109 | }
110 | }
111 | };
112 | }
113 |
114 | // 쿼리 파라미터 순서가 바뀌어도 같은 key가 나오도록 키 이름으로 정렬한다
115 | export function getApiCacheKey(actionType, { apiHost, url, params }) {
116 | const prefix = `${actionType}_${apiHost ? apiHost + url : url}`;
117 | const keys = params ? Object.keys(params) : [];
118 | if (keys.length) {
119 | return (
120 | prefix +
121 | keys.sort().reduce((acc, key) => `${acc}&${key}=${params[key]}`, '')
122 | );
123 | } else {
124 | return prefix;
125 | }
126 | }
127 |
128 | export function getFetchKey(action) {
129 | const fetchKey = action[FETCH_KEY];
130 | return fetchKey === undefined ? action.type : String(fetchKey);
131 | }
132 |
133 | function getIsGeneratorFunction(obj) {
134 | const constructor = obj.constructor;
135 | if (!constructor) {
136 | return false;
137 | }
138 | if (
139 | 'GeneratorFunction' === constructor.name ||
140 | 'GeneratorFunction' === constructor.displayName
141 | ) {
142 | return true;
143 | }
144 | const proto = constructor.prototype;
145 | return 'function' === typeof proto.next && 'function' === typeof proto.throw;
146 | }
147 |
148 | /**
149 | *
150 | * @param {string=} actionType
151 | */
152 | export function deleteApiCache(actionType) {
153 | let keys = apiCache.keys();
154 | if (actionType) {
155 | keys = keys.filter(key => key.includes(actionType));
156 | }
157 | for (const key of keys) {
158 | apiCache.del(key);
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/whois/start/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import store from './common/store';
5 | import { Provider } from 'react-redux';
6 | import { BrowserRouter } from 'react-router-dom';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 | ,
14 | document.getElementById('root'),
15 | );
16 |
--------------------------------------------------------------------------------
/whois/start/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------