├── .github
└── ISSUE_TEMPLATE
│ └── 북스터디-이슈-템플릿.md
├── .gitignore
├── README.md
└── pickme
├── .idea
├── misc.xml
├── modules.xml
├── pickme.iml
├── vcs.xml
└── workspace.xml
├── KangJiJi
├── .babelrc
├── .eslintignore.js
├── .eslintrc.js
├── .postcssrc
├── index.css
├── index.html
├── index.js
├── package-lock.json
├── package.json
└── src
│ ├── app.js
│ ├── components
│ ├── increaseParticipantButtonComponent.js
│ ├── pickPresenterButtonComponent.js
│ ├── setInformationOfParticipantComponent.js
│ └── showPresenterComponent.js
│ ├── helper
│ ├── domHelper.js
│ └── presenterPickHelper.js
│ └── pages
│ └── mainPage.js
├── hyesun03
├── .gitignore
├── .idea
│ ├── codeStyles
│ │ └── codeStyleConfig.xml
│ ├── hyesun03.iml
│ ├── misc.xml
│ ├── modules.xml
│ └── vcs.xml
├── crawling.py
├── crawling_test.json
├── main.py
├── requirements.txt
├── study_member.json
└── test.py
└── jisoo
├── index.html
└── spec.md
/.github/ISSUE_TEMPLATE/북스터디-이슈-템플릿.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 북스터디 이슈 템플릿
3 | about: 다음 스터디의 범위를 제목에 입력해주세요~
4 | title: "[책 제목] 23.00.00/0주차 - 챕터 0. 챕터 제목까지 ~ 000p"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | 스터디는 마이크를 끄지 않아요.
11 |
12 | 8시까지 해당 이슈에 책을 읽으면서 궁금했던 부분, 함께 이야기해보면 좋을것 같은 부분을 코멘트로 달아주세요.
13 | 아무것도 안올리면 그 다음회차 호스트하기
14 |
15 | 호스트는 우선적으로 스터디원들이 올린 코멘트에 대해 자신의 생각을 이야기하고, 다른 사람들은 어떻게 생각하는지 토론을 이끌어 나가는 역할을 수행합니다.
16 |
17 | .
18 | 우리 스터디에 멍청한 질문은 없습니다.
19 | 내가 이해 안 간 부분이 있다면 그건 높은 확률로 다른 사람도 이해를 못했을 것입니다.
20 |
21 | 우리 스터디는 이걸 위해서 진행합니다. 혼자서 읽었다면 무심코 넘어갔을 부분을 다양한 사람들이 모여 다양한 시각에서 바라보고 자신의 단어로 토론하고 이해하는걸 목표로 합니다.
22 |
23 | 그렇기 때문에 스터디때 질문하지 않고 의견을 내지 않는건 나 뿐만 아니라 스터디원 전체의 목표에 어긋나는 일이란걸 인지하고 "이런걸 물어봐도 되나?" 걱정하지 말고 마구마구 얘기하면 좋습니다.
24 |
25 | 그래서 우리 스터디에선 멍청한 질문은 없습니다. 모든 질문이 문제를 바라보는 새로운 관점일 뿐~
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## HTTP Ignore file ##
2 |
3 | # Parcel
4 | .cache
5 | dist
6 | node_modules
7 | build
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # http
2 | TCP 북 스터디
3 |
4 | ### 범위
5 | 다음 책들을 반복해서 읽어나가요.
6 |
7 | ##### 스터디에서 읽을 책.
8 | - 클린코드
9 | - 객체지향의 사실과 오해
10 | - 리팩터링
11 | - TDD
12 | - 오브젝트
13 | - GoF의 디자인 패턴
14 | - DDD
15 | - 클린아키텍처
16 |
17 | ##### 개인적으로 곁들여 읽으면 좋은 책.
18 | - 소프트웨어 장인
19 | - 실용주의 프로그래머
20 | - 맨먼스 미신
21 | - 익스트림 프로그래밍
22 | - 코딩 호러가 들려주는 진짜 소프트웨어 개발 이야기
23 | - 코딩 호러의 이펙티브 프로그래밍
24 | - 조엘 온 소프트웨어
25 |
26 | ### ❓ 진행방식
27 | - 매주 일요일 오후9시 디스코드에서 만나 온라인으로 진행합니다.
28 | - 스터디 동안에는 마이크를 끄지 않습니다.
29 | - 정해진 분량을 각자 읽고, 이해가 되지 않는 부분이나 저자의 의견에 동의가 되지 않는 부분, 혹은 다른 사람의 생각이 궁금한 부분을 코멘트로 남깁니다.
30 | - 한명의 드라이버를 정하고, 드라이버는 해당 코멘트들을 소개하고 토론을 진행하는 역할을 맡습니다.
31 | - 여느 스터디와 같이 책 내용을 요약하고 발표하거나 하진 않아요.
32 |
33 |
34 | ### ❗ 우리 스터디에 멍청한 질문은 없습니다
35 | - 내가 이해 안 간 부분이 있다면 그건 높은 확률로 다른 사람도 이해를 못했을 것입니다.
36 | - 우리 스터디는 이걸 위해서 진행합니다.
37 | - 혼자서 읽었다면 무심코 넘어갔을 부분을 다양한 사람들이 모여 다양한 시각에서 바라보고 자신의 단어로 토론하고 이해하는걸 목표로 합니다.
38 | - 그렇기 때문에 스터디때 질문하지 않고 의견을 내지 않는건 나 뿐만 아니라 스터디원 전체의 목표에 어긋나는 일이란걸 인지하고
39 | - "이런걸 물어봐도 되나?" 걱정하지 말고 마구마구 얘기하면 좋습니다.
40 | - 그래서 우리 스터디에선 멍청한 질문은 없습니다. 모든 질문이 문제를 바라보는 새로운 관점일 뿐
41 |
--------------------------------------------------------------------------------
/pickme/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | CSS
13 |
14 |
15 | HTML
16 |
17 |
18 | Probable bugsCSS
19 |
20 |
21 | RELAX NG
22 |
23 |
24 | SQL
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/pickme/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/pickme/.idea/pickme.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/pickme/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/pickme/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
56 |
57 |
58 |
59 |
60 | 1569395439308
61 |
62 |
63 | 1569395439308
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/pickme/KangJiJi/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"]
3 | }
--------------------------------------------------------------------------------
/pickme/KangJiJi/.eslintignore.js:
--------------------------------------------------------------------------------
1 | /node_modules/**
2 | /.cache/**
3 | /dist/**
4 | /.babelrc
5 | /.postcssrc
6 | /package-lock.json
7 | /package.json
--------------------------------------------------------------------------------
/pickme/KangJiJi/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "es6": true
6 | },
7 | "extends": "airbnb-base",
8 | "globals": {
9 | "Atomics": "readonly",
10 | "SharedArrayBuffer": "readonly"
11 | },
12 | "parserOptions": {
13 | "ecmaVersion": 2018,
14 | "sourceType": "module"
15 | },
16 | "plugins": ['import'],
17 | "rules": {
18 | "max-length": 1,
19 | "linebreak-style": 0,
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/pickme/KangJiJi/.postcssrc:
--------------------------------------------------------------------------------
1 | {
2 | "modules": true,
3 | "plugins": {
4 | "autoprefixer": {
5 | "grid": true
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/pickme/KangJiJi/index.css:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0-modified | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html, body, div, span, applet, object, iframe,
7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8 | a, abbr, acronym, address, big, cite, code,
9 | del, dfn, em, img, ins, kbd, q, s, samp,
10 | small, strike, strong, sub, sup, tt, var,
11 | b, u, i, center,
12 | dl, dt, dd, ol, ul, li,
13 | fieldset, form, label, legend,
14 | table, caption, tbody, tfoot, thead, tr, th, td,
15 | article, aside, canvas, details, embed,
16 | figure, figcaption, footer, header, hgroup,
17 | menu, nav, output, ruby, section, summary,
18 | time, mark, audio, video {
19 | margin: 0;
20 | padding: 0;
21 | border: 0;
22 | font-size: 100%;
23 | font: inherit;
24 | vertical-align: baseline;
25 | }
26 |
27 | /* make sure to set some focus styles for accessibility */
28 | :focus {
29 | outline: 0;
30 | }
31 |
32 | /* HTML5 display-role reset for older browsers */
33 | article, aside, details, figcaption, figure,
34 | footer, header, hgroup, menu, nav, section {
35 | display: block;
36 | }
37 |
38 | body {
39 | line-height: 1;
40 | }
41 |
42 | ol, ul {
43 | list-style: none;
44 | }
45 |
46 | blockquote, q {
47 | quotes: none;
48 | }
49 |
50 | blockquote:before, blockquote:after,
51 | q:before, q:after {
52 | content: '';
53 | content: none;
54 | }
55 |
56 | table {
57 | border-collapse: collapse;
58 | border-spacing: 0;
59 | }
60 |
61 | input[type=search]::-webkit-search-cancel-button,
62 | input[type=search]::-webkit-search-decoration,
63 | input[type=search]::-webkit-search-results-button,
64 | input[type=search]::-webkit-search-results-decoration {
65 | -webkit-appearance: none;
66 | -moz-appearance: none;
67 | }
68 |
69 | input[type=search] {
70 | -webkit-appearance: none;
71 | -moz-appearance: none;
72 | -webkit-box-sizing: content-box;
73 | -moz-box-sizing: content-box;
74 | box-sizing: content-box;
75 | }
76 |
77 | textarea {
78 | overflow: auto;
79 | vertical-align: top;
80 | resize: vertical;
81 | }
82 |
83 | /**
84 | * Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
85 | */
86 |
87 | audio,
88 | canvas,
89 | video {
90 | display: inline-block;
91 | *display: inline;
92 | *zoom: 1;
93 | max-width: 100%;
94 | }
95 |
96 | /**
97 | * Prevent modern browsers from displaying `audio` without controls.
98 | * Remove excess height in iOS 5 devices.
99 | */
100 |
101 | audio:not([controls]) {
102 | display: none;
103 | height: 0;
104 | }
105 |
106 | /**
107 | * Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
108 | * Known issue: no IE 6 support.
109 | */
110 |
111 | [hidden] {
112 | display: none;
113 | }
114 |
115 | /**
116 | * 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
117 | * `em` units.
118 | * 2. Prevent iOS text size adjust after orientation change, without disabling
119 | * user zoom.
120 | */
121 |
122 | html {
123 | font-size: 100%; /* 1 */
124 | -webkit-text-size-adjust: 100%; /* 2 */
125 | -ms-text-size-adjust: 100%; /* 2 */
126 | }
127 |
128 | /**
129 | * Address `outline` inconsistency between Chrome and other browsers.
130 | */
131 |
132 | a:focus {
133 | outline: thin dotted;
134 | }
135 |
136 | /**
137 | * Improve readability when focused and also mouse hovered in all browsers.
138 | */
139 |
140 | a:active,
141 | a:hover {
142 | outline: 0;
143 | }
144 |
145 | /**
146 | * 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
147 | * 2. Improve image quality when scaled in IE 7.
148 | */
149 |
150 | img {
151 | border: 0; /* 1 */
152 | -ms-interpolation-mode: bicubic; /* 2 */
153 | }
154 |
155 | /**
156 | * Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
157 | */
158 |
159 | figure {
160 | margin: 0;
161 | }
162 |
163 | /**
164 | * Correct margin displayed oddly in IE 6/7.
165 | */
166 |
167 | form {
168 | margin: 0;
169 | }
170 |
171 | /**
172 | * Define consistent border, margin, and padding.
173 | */
174 |
175 | fieldset {
176 | border: 1px solid #c0c0c0;
177 | margin: 0 2px;
178 | padding: 0.35em 0.625em 0.75em;
179 | }
180 |
181 | /**
182 | * 1. Correct color not being inherited in IE 6/7/8/9.
183 | * 2. Correct text not wrapping in Firefox 3.
184 | * 3. Correct alignment displayed oddly in IE 6/7.
185 | */
186 |
187 | legend {
188 | border: 0; /* 1 */
189 | padding: 0;
190 | white-space: normal; /* 2 */
191 | *margin-left: -7px; /* 3 */
192 | }
193 |
194 | /**
195 | * 1. Correct font size not being inherited in all browsers.
196 | * 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
197 | * and Chrome.
198 | * 3. Improve appearance and consistency in all browsers.
199 | */
200 |
201 | button,
202 | input,
203 | select,
204 | textarea {
205 | font-size: 100%; /* 1 */
206 | margin: 0; /* 2 */
207 | vertical-align: baseline; /* 3 */
208 | *vertical-align: middle; /* 3 */
209 | }
210 |
211 | /**
212 | * Address Firefox 3+ setting `line-height` on `input` using `!important` in
213 | * the UA stylesheet.
214 | */
215 |
216 | button,
217 | input {
218 | line-height: normal;
219 | }
220 |
221 | /**
222 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
223 | * All other form control elements do not inherit `text-transform` values.
224 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
225 | * Correct `select` style inheritance in Firefox 4+ and Opera.
226 | */
227 |
228 | button,
229 | select {
230 | text-transform: none;
231 | }
232 |
233 | /**
234 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
235 | * and `video` controls.
236 | * 2. Correct inability to style clickable `input` types in iOS.
237 | * 3. Improve usability and consistency of cursor style between image-type
238 | * `input` and others.
239 | * 4. Remove inner spacing in IE 7 without affecting normal text inputs.
240 | * Known issue: inner spacing remains in IE 6.
241 | */
242 |
243 | button,
244 | html input[type="button"], /* 1 */
245 | input[type="reset"],
246 | input[type="submit"] {
247 | -webkit-appearance: button; /* 2 */
248 | cursor: pointer; /* 3 */
249 | *overflow: visible; /* 4 */
250 | }
251 |
252 | /**
253 | * Re-set default cursor for disabled elements.
254 | */
255 |
256 | button[disabled],
257 | html input[disabled] {
258 | cursor: default;
259 | }
260 |
261 | /**
262 | * 1. Address box sizing set to content-box in IE 8/9.
263 | * 2. Remove excess padding in IE 8/9.
264 | * 3. Remove excess padding in IE 7.
265 | * Known issue: excess padding remains in IE 6.
266 | */
267 |
268 | input[type="checkbox"],
269 | input[type="radio"] {
270 | box-sizing: border-box; /* 1 */
271 | padding: 0; /* 2 */
272 | *height: 13px; /* 3 */
273 | *width: 13px; /* 3 */
274 | }
275 |
276 | /**
277 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
278 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
279 | * (include `-moz` to future-proof).
280 | */
281 |
282 | input[type="search"] {
283 | -webkit-appearance: textfield; /* 1 */
284 | -moz-box-sizing: content-box;
285 | -webkit-box-sizing: content-box; /* 2 */
286 | box-sizing: content-box;
287 | }
288 |
289 | /**
290 | * Remove inner padding and search cancel button in Safari 5 and Chrome
291 | * on OS X.
292 | */
293 |
294 | input[type="search"]::-webkit-search-cancel-button,
295 | input[type="search"]::-webkit-search-decoration {
296 | -webkit-appearance: none;
297 | }
298 |
299 | /**
300 | * Remove inner padding and border in Firefox 3+.
301 | */
302 |
303 | button::-moz-focus-inner,
304 | input::-moz-focus-inner {
305 | border: 0;
306 | padding: 0;
307 | }
308 |
309 | /**
310 | * 1. Remove default vertical scrollbar in IE 6/7/8/9.
311 | * 2. Improve readability and alignment in all browsers.
312 | */
313 |
314 | textarea {
315 | overflow: auto; /* 1 */
316 | vertical-align: top; /* 2 */
317 | }
318 |
319 | /**
320 | * Remove most spacing between table cells.
321 | */
322 |
323 | table {
324 | border-collapse: collapse;
325 | border-spacing: 0;
326 | }
327 |
328 | html,
329 | button,
330 | input,
331 | select,
332 | textarea {
333 | color: #222;
334 | }
335 |
336 |
337 | ::-moz-selection {
338 | background: #b3d4fc;
339 | text-shadow: none;
340 | }
341 |
342 | ::selection {
343 | background: #b3d4fc;
344 | text-shadow: none;
345 | }
346 |
347 | img {
348 | vertical-align: middle;
349 | }
350 |
351 | fieldset {
352 | border: 0;
353 | margin: 0;
354 | padding: 0;
355 | }
356 |
357 | textarea {
358 | resize: vertical;
359 | }
360 |
361 | .chromeframe {
362 | margin: 0.2em 0;
363 | background: #ccc;
364 | color: #000;
365 | padding: 0.2em 0;
366 | }
367 |
--------------------------------------------------------------------------------
/pickme/KangJiJi/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Pick Me
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/pickme/KangJiJi/index.js:
--------------------------------------------------------------------------------
1 | import app from './src/app';
2 |
3 | window.addEventListener('load', app.render.bind(app));
4 |
--------------------------------------------------------------------------------
/pickme/KangJiJi/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kangjiji",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "dependencies": {
7 | "@babel/preset-env": "^7.6.2",
8 | "autoprefixer": "^9.6.4",
9 | "parcel-bundler": "^1.12.3",
10 | "postcss-modules": "^1.4.1"
11 | },
12 | "devDependencies": {
13 | "@babel/core": "^7.6.2",
14 | "eslint": "^6.6.0",
15 | "eslint-config-airbnb-base": "^14.0.0",
16 | "eslint-plugin-import": "^2.18.2"
17 | },
18 | "browserslist": [
19 | "last 1 Chrome versions"
20 | ],
21 | "scripts": {
22 | "test": "echo \"Error: no test specified\" && exit 1",
23 | "lint": "eslint .",
24 | "start": "parcel index.html"
25 | },
26 | "author": "",
27 | "license": "ISC"
28 | }
29 |
--------------------------------------------------------------------------------
/pickme/KangJiJi/src/app.js:
--------------------------------------------------------------------------------
1 | import mainPage from './pages/mainPage';
2 | import domHelper from './helper/domHelper';
3 |
4 | // Routing
5 | const app = {
6 | render() {
7 | // Add routing
8 | this.pageChange(mainPage);
9 | },
10 | pageChange(pageComponent) {
11 | this.pageRendering(pageComponent);
12 | },
13 | pageRendering(pageComponent) {
14 | if (pageComponent && pageComponent.render) {
15 | domHelper.initDom();
16 | pageComponent.render();
17 | }
18 | },
19 | };
20 |
21 | export default app;
22 |
--------------------------------------------------------------------------------
/pickme/KangJiJi/src/components/increaseParticipantButtonComponent.js:
--------------------------------------------------------------------------------
1 | import domHelper from '../helper/domHelper';
2 |
3 | const increaseParticipantButtonComponent = {
4 | componentHtml: `
5 |
6 | `,
7 | componentClassName: 'set-num-of-participant-component',
8 | eventHandler: Function.prototype,
9 | getComponentHtml() {
10 | return this.componentHtml;
11 | },
12 | getComponentClassName() {
13 | return this.componentClassName;
14 | },
15 | setEventHandler(eventHandler) {
16 | this.eventHandler = eventHandler;
17 | },
18 | increaseNumOfParticipant() {
19 | return this.eventHandler();
20 | },
21 | addEvent() {
22 | const button = document.getElementsByClassName('set-num-of-participant-component__num-submit-button')[0];
23 | button.addEventListener('click', this.increaseNumOfParticipant.bind(this));
24 | },
25 | render(eventHandler) {
26 | this.setEventHandler(eventHandler);
27 | domHelper.render(this);
28 | },
29 | };
30 |
31 | export default increaseParticipantButtonComponent;
32 |
--------------------------------------------------------------------------------
/pickme/KangJiJi/src/components/pickPresenterButtonComponent.js:
--------------------------------------------------------------------------------
1 | import domHelper from '../helper/domHelper';
2 |
3 | const pickPresenterButtonComponent = {
4 | componentHtml: '',
5 | componentClassName: 'pick-presenter-component',
6 | eventHandler: Function.prototype,
7 | getComponentHtml() {
8 | return this.componentHtml;
9 | },
10 | getComponentClassName() {
11 | return this.componentClassName;
12 | },
13 | setEventHandler(eventHandler) {
14 | this.eventHandler = eventHandler;
15 | },
16 | pickPresenter() {
17 | return this.eventHandler();
18 | },
19 | addEvent() {
20 | const button = document.getElementsByClassName('pick-presenter-component__pick-presenter-button')[0];
21 | button.addEventListener('click', this.pickPresenter.bind(this));
22 | },
23 | render(eventHandler) {
24 | this.setEventHandler(eventHandler);
25 | domHelper.render(this);
26 | },
27 | };
28 |
29 | export default pickPresenterButtonComponent;
30 |
--------------------------------------------------------------------------------
/pickme/KangJiJi/src/components/setInformationOfParticipantComponent.js:
--------------------------------------------------------------------------------
1 | import domHelper from '../helper/domHelper';
2 |
3 | const setInformationOfParticipantComponent = {
4 | componentHtml: `
5 |
6 |
7 |
`,
8 | componentClassName: 'set-information-of-participant-component',
9 | eventHandler: Function.prototype,
10 | getComponentHtml() {
11 | return this.componentHtml;
12 | },
13 | getComponentClassName() {
14 | return this.componentClassName;
15 | },
16 | setEventHandler(eventHandler) {
17 | this.eventHandler = eventHandler;
18 | },
19 | getInformationOfParticipants() {
20 | const informationOfParticipantWrapperDom = document.getElementsByClassName(this.getComponentClassName())[0];
21 | const informationOfParticipantDom = Array.from(informationOfParticipantWrapperDom.children);
22 | const informationOfParticipants = informationOfParticipantDom.map(function getInformationOfParticipantFromDom(dom) {
23 | const informationOfParticipant = [dom.children[0].value, Number(dom.children[1].value)];
24 | return informationOfParticipant;
25 | });
26 | return informationOfParticipants;
27 | },
28 | appendComponent() {
29 | domHelper.render(this, this.getComponentClassName());
30 | },
31 | addEvent() { },
32 | render(eventHandler) {
33 | this.setEventHandler(eventHandler);
34 | domHelper.render(this);
35 | },
36 | };
37 |
38 | export default setInformationOfParticipantComponent;
39 |
--------------------------------------------------------------------------------
/pickme/KangJiJi/src/components/showPresenterComponent.js:
--------------------------------------------------------------------------------
1 | import domHelper from '../helper/domHelper';
2 |
3 | const showPresenterComponent = {
4 | componentHtml: '',
5 | componentClassName: 'show-presenter-component',
6 | eventHandler: Function.prototype,
7 | getComponentHtml() {
8 | return this.componentHtml;
9 | },
10 | getComponentClassName() {
11 | return this.componentClassName;
12 | },
13 | setEventHandler(eventHandler) {
14 | this.eventHandler = eventHandler;
15 | },
16 | setPresenter(presenter) {
17 | document.getElementsByClassName('show-presenter-component__presenter-name')[0].innerHTML = presenter;
18 | },
19 | addEvent() { },
20 | render(eventHandler) {
21 | this.setEventHandler(eventHandler);
22 | domHelper.render(this);
23 | },
24 | };
25 |
26 | export default showPresenterComponent;
27 |
--------------------------------------------------------------------------------
/pickme/KangJiJi/src/helper/domHelper.js:
--------------------------------------------------------------------------------
1 | const domHelper = {
2 | render(component, parentClassName) {
3 | this.appendHtml(component, parentClassName)
4 | .then(this.addEvent)
5 | .catch(function errorHandling(err) {
6 | console.log('Error: ', err);
7 | });
8 | },
9 | appendHtml(component, parentClassName) {
10 | return new Promise(function appendHtml(resolve, reject) {
11 | if (!component.getComponentHtml) {
12 | reject(new Error('This component does not have getComponentHtml method'));
13 | }
14 |
15 | const parentComponent = parentClassName
16 | ? document.getElementsByClassName(parentClassName)[0]
17 | : document.getElementById('app');
18 |
19 | const divWrapper = document.createElement('div');
20 | divWrapper.className = component.getComponentClassName();
21 | divWrapper.innerHTML = component.getComponentHtml();
22 |
23 | if (!parentClassName) {
24 | parentComponent.appendChild(divWrapper);
25 | } else {
26 | parentComponent.appendChild(divWrapper.children[0]);
27 | }
28 |
29 | resolve(component);
30 | });
31 | },
32 | addEvent(component) {
33 | return new Promise(function addEvent(resolve, reject) {
34 | if (!component.addEvent) {
35 | reject(new Error('This component does not have addEvent method'));
36 | }
37 | component.addEvent();
38 | resolve(component);
39 | });
40 | },
41 | removeHtml() {
42 | console.log('This function remove component by class name');
43 | },
44 | initDom() {
45 | document.getElementById('app').innerHTML = '';
46 | },
47 | };
48 |
49 | export default domHelper;
50 |
--------------------------------------------------------------------------------
/pickme/KangJiJi/src/helper/presenterPickHelper.js:
--------------------------------------------------------------------------------
1 | const presenterPickHelper = {
2 | pickPresenter(informationOfParticipants) {
3 | const informationOfParticipantsWithPercentage = this.anotherEqualityCalculatePercentage(informationOfParticipants);
4 | const presenter = this.randPickWithPercentage(informationOfParticipantsWithPercentage);
5 | return presenter;
6 | },
7 | equalityCalculatePercentage(informationOfParticipants) {
8 | let equalityPercent = 0;
9 | const lengthOfInformationOfParticipants = informationOfParticipants.length;
10 | const sortedInformationOfParticipants = [...informationOfParticipants].sort(function descSort(a, b) {
11 | return b[1] - a[1];
12 | });
13 |
14 | equalityPercent = 100 / lengthOfInformationOfParticipants;
15 |
16 | sortedInformationOfParticipants.forEach(function changePresentationTimesToPercentage(informationOfParticipant) {
17 | informationOfParticipant[1] = equalityPercent;
18 | });
19 | return sortedInformationOfParticipants;
20 | },
21 | anotherEqualityCalculatePercentage(informationOfParticipants) {
22 | const sortedInformationOfParticipants = [...informationOfParticipants].sort(function ascSort(a, b) {
23 | return a[1] - b[1];
24 | });
25 |
26 | sortedInformationOfParticipants.forEach(function changePresentationTimesToPercentage(informationOfParticipant, index) {
27 | if (index === 0) {
28 | informationOfParticipant[1] = 100;
29 | } else {
30 | informationOfParticipant[1] = 0;
31 | }
32 | });
33 |
34 | return sortedInformationOfParticipants;
35 | },
36 | randPickWithPercentage(informationOfParticipants) {
37 | const listLength = informationOfParticipants.length;
38 | const rand = Math.floor(Math.random() * 100) + 1;
39 | let sumOfPercentage = 0;
40 |
41 | for (let i = 0; i < listLength; i += 1) {
42 | sumOfPercentage += informationOfParticipants[i][1];
43 | if (rand <= sumOfPercentage) {
44 | return informationOfParticipants[i][0];
45 | }
46 | }
47 |
48 | return false;
49 | },
50 | };
51 |
52 | export default presenterPickHelper;
53 |
--------------------------------------------------------------------------------
/pickme/KangJiJi/src/pages/mainPage.js:
--------------------------------------------------------------------------------
1 | import increaseParticipantButtonComponent from '../components/increaseParticipantButtonComponent';
2 | import setInformationOfParticipantComponent from '../components/setInformationOfParticipantComponent';
3 | import pickPresenterButtonComponent from '../components/pickPresenterButtonComponent';
4 | import showPresenterComponent from '../components/showPresenterComponent';
5 | import presenterPickHelper from '../helper/presenterPickHelper';
6 |
7 | const mainPage = {
8 | appendSetInformationOfParticipantComponent() {
9 | setInformationOfParticipantComponent.appendComponent();
10 | },
11 | increaseParticipant() {
12 | this.appendSetInformationOfParticipantComponent();
13 | },
14 | showPresenter(presenterName) {
15 | showPresenterComponent.setPresenter(presenterName);
16 | },
17 | pickPresenter() {
18 | const informationOfParticipants = setInformationOfParticipantComponent.getInformationOfParticipants();
19 | const presenterName = presenterPickHelper.pickPresenter(informationOfParticipants);
20 | this.showPresenter(presenterName);
21 | },
22 | render() {
23 | increaseParticipantButtonComponent.render(this.increaseParticipant.bind(this));
24 | setInformationOfParticipantComponent.render();
25 | pickPresenterButtonComponent.render(this.pickPresenter.bind(this));
26 | showPresenterComponent.render();
27 | },
28 | };
29 |
30 | export default mainPage;
31 |
--------------------------------------------------------------------------------
/pickme/hyesun03/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Python template
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | pip-wheel-metadata/
26 | share/python-wheels/
27 | *.egg-info/
28 | .installed.cfg
29 | *.egg
30 | MANIFEST
31 |
32 | # PyInstaller
33 | # Usually these files are written by a python script from a template
34 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
35 | *.manifest
36 | *.spec
37 |
38 | # Installer logs
39 | pip-log.txt
40 | pip-delete-this-directory.txt
41 |
42 | # Unit test / coverage reports
43 | htmlcov/
44 | .tox/
45 | .nox/
46 | .coverage
47 | .coverage.*
48 | .cache
49 | nosetests.xml
50 | coverage.xml
51 | *.cover
52 | .hypothesis/
53 | .pytest_cache/
54 |
55 | # Translations
56 | *.mo
57 | *.pot
58 |
59 | # Django stuff:
60 | *.log
61 | local_settings.py
62 | db.sqlite3
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # celery beat schedule file
95 | celerybeat-schedule
96 |
97 | # SageMath parsed files
98 | *.sage.py
99 |
100 | # Environments
101 | .env
102 | .venv
103 | env/
104 | venv/
105 | ENV/
106 | env.bak/
107 | venv.bak/
108 |
109 | # Spyder project settings
110 | .spyderproject
111 | .spyproject
112 |
113 | # Rope project settings
114 | .ropeproject
115 |
116 | # mkdocs documentation
117 | /site
118 |
119 | # mypy
120 | .mypy_cache/
121 | .dmypy.json
122 | dmypy.json
123 |
124 | # Pyre type checker
125 | .pyre/
126 |
127 | ### macOS template
128 | # General
129 | .DS_Store
130 | .AppleDouble
131 | .LSOverride
132 |
133 | # Icon must end with two \r
134 | Icon
135 |
136 | # Thumbnails
137 | ._*
138 |
139 | # Files that might appear in the root of a volume
140 | .DocumentRevisions-V100
141 | .fseventsd
142 | .Spotlight-V100
143 | .TemporaryItems
144 | .Trashes
145 | .VolumeIcon.icns
146 | .com.apple.timemachine.donotpresent
147 |
148 | # Directories potentially created on remote AFP share
149 | .AppleDB
150 | .AppleDesktop
151 | Network Trash Folder
152 | Temporary Items
153 | .apdisk
154 |
155 | ### JetBrains template
156 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
157 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
158 |
159 | # User-specific stuff
160 | .idea/**/workspace.xml
161 | .idea/**/tasks.xml
162 | .idea/**/usage.statistics.xml
163 | .idea/**/dictionaries
164 | .idea/**/shelf
165 |
166 | # Generated files
167 | .idea/**/contentModel.xml
168 |
169 | # Sensitive or high-churn files
170 | .idea/**/dataSources/
171 | .idea/**/dataSources.ids
172 | .idea/**/dataSources.local.xml
173 | .idea/**/sqlDataSources.xml
174 | .idea/**/dynamic.xml
175 | .idea/**/uiDesigner.xml
176 | .idea/**/dbnavigator.xml
177 |
178 | # Gradle
179 | .idea/**/gradle.xml
180 | .idea/**/libraries
181 |
182 | # Gradle and Maven with auto-import
183 | # When using Gradle or Maven with auto-import, you should exclude module files,
184 | # since they will be recreated, and may cause churn. Uncomment if using
185 | # auto-import.
186 | # .idea/modules.xml
187 | # .idea/*.iml
188 | # .idea/modules
189 | # *.iml
190 | # *.ipr
191 |
192 | # CMake
193 | cmake-build-*/
194 |
195 | # Mongo Explorer plugin
196 | .idea/**/mongoSettings.xml
197 |
198 | # File-based project format
199 | *.iws
200 |
201 | # IntelliJ
202 | out/
203 |
204 | # mpeltonen/sbt-idea plugin
205 | .idea_modules/
206 |
207 | # JIRA plugin
208 | atlassian-ide-plugin.xml
209 |
210 | # Cursive Clojure plugin
211 | .idea/replstate.xml
212 |
213 | # Crashlytics plugin (for Android Studio and IntelliJ)
214 | com_crashlytics_export_strings.xml
215 | crashlytics.properties
216 | crashlytics-build.properties
217 | fabric.properties
218 |
219 | # Editor-based Rest Client
220 | .idea/httpRequests
221 |
222 | # Android studio 3.1+ serialized cache file
223 | .idea/caches/build_file_checksums.ser
224 |
225 |
--------------------------------------------------------------------------------
/pickme/hyesun03/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/pickme/hyesun03/.idea/hyesun03.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/pickme/hyesun03/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | CSS
13 |
14 |
15 | HTML
16 |
17 |
18 | Probable bugsCSS
19 |
20 |
21 | RELAX NG
22 |
23 |
24 | SQL
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/pickme/hyesun03/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/pickme/hyesun03/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/pickme/hyesun03/crawling.py:
--------------------------------------------------------------------------------
1 | from typing import Dict
2 | import requests
3 | import json
4 |
5 | from bs4 import BeautifulSoup as bs
6 |
7 |
8 | def crawl_roll_book() -> Dict:
9 | r = requests.get("https://github.com/TeamCrazyPerformance/http/wiki/%5BGoF-%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4%5D-%EC%B6%9C%EC%84%9D%EB%B6%80")
10 |
11 | content = bs(r.text, 'html.parser')
12 |
13 | table = content.find('table')
14 | thead = table.select('thead > tr > th')
15 | names = [item.text for item in thead][1:]
16 |
17 | data = {"members": []}
18 | members = data["members"]
19 | for i in range(len(names)):
20 | members.append({"name": names[i], "speak": [], "wiki": [], "absent": []})
21 |
22 | tbody = table.select('tbody > tr')
23 |
24 | for row in tbody:
25 | row = row.select('tr > td')
26 | date = row[0].text.replace('.', '-')
27 | row = row[1:]
28 |
29 | for i in range(len(names)):
30 | if row[i].text == 'X':
31 | members[i]["absent"].append(date)
32 |
33 | return data
34 |
35 |
36 | def to_json(data: Dict) -> None:
37 | with open('crawling_test.json', 'w', encoding='utf-8') as file:
38 | json.dump(data, file, ensure_ascii=False, indent='\t')
39 |
40 |
41 | if __name__ == '__main__':
42 | data = crawl_roll_book()
43 | to_json(data)
44 |
45 | with open("crawling_test.json") as file:
46 | data = json.load(file)
47 | study_member = data["members"]
48 |
49 | # study_member.json과 똑같은 포맷으로 만들어진 것을 볼 수 있음
50 | print(study_member)
51 |
--------------------------------------------------------------------------------
/pickme/hyesun03/crawling_test.json:
--------------------------------------------------------------------------------
1 | {
2 | "members": [
3 | {
4 | "name": "강지훈",
5 | "speak": [],
6 | "wiki": [],
7 | "absent": []
8 | },
9 | {
10 | "name": "김일범",
11 | "speak": [],
12 | "wiki": [],
13 | "absent": [
14 | "19-07-14",
15 | "19-09-08"
16 | ]
17 | },
18 | {
19 | "name": "김희라",
20 | "speak": [],
21 | "wiki": [],
22 | "absent": []
23 | },
24 | {
25 | "name": "윤지수",
26 | "speak": [],
27 | "wiki": [],
28 | "absent": []
29 | },
30 | {
31 | "name": "이건우",
32 | "speak": [],
33 | "wiki": [],
34 | "absent": []
35 | },
36 | {
37 | "name": "이재란",
38 | "speak": [],
39 | "wiki": [],
40 | "absent": [
41 | "19-07-07",
42 | "19-08-04",
43 | "19-09-01"
44 | ]
45 | },
46 | {
47 | "name": "최혜선",
48 | "speak": [],
49 | "wiki": [],
50 | "absent": []
51 | }
52 | ]
53 | }
--------------------------------------------------------------------------------
/pickme/hyesun03/main.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | import json
3 | import random
4 | import click
5 |
6 |
7 | def read_json() -> List[list]:
8 | """스터디 참석자에 대한 정보가 담긴 json 파일을 읽어옵니다."""
9 |
10 | with open("study_member.json") as file:
11 | data = json.load(file)
12 | study_member = data["members"]
13 |
14 | name, speak, wiki, absent = [], [], [], []
15 | for member in study_member:
16 | name.append(member["name"])
17 | speak.append(len(member["speak"]))
18 | wiki.append(len(member["wiki"]))
19 | absent.append(len(member["absent"]))
20 |
21 | return [name, speak, wiki, absent]
22 |
23 |
24 | @click.command()
25 | @click.option('--today_absent', prompt="오늘 결석한 사람들", help="발표자와 서기를 뽑습니다.")
26 | def pickme(today_absent: str) -> None:
27 | """오늘 결석자에 대한 정보를 인자로 받고, 발표자와 서기를 뽑습니다."""
28 |
29 | name, speak, wiki, absent = read_json()
30 | today_absent = today_absent.strip().split()
31 |
32 | if len(today_absent) > len(name) - 2:
33 | print("<< 축 하 합 니 다 >>")
34 | print("오늘은 스터디 쉬는날!")
35 | print("<< 축 하 합 니 다 >>")
36 | return
37 |
38 | # 현재까지 진행한 스터디 횟수 == 발표 횟수
39 | total = len(speak)
40 |
41 | # 1 - ((내가 걸린 횟수 - 내가 결석한 횟수) / 여태 진행했던 횟수) 로 가중치 적용
42 | speak = [1 - (speak[i] - absent[i]) / total for i in range(len(name))]
43 | wiki = [1 - (wiki[i] - absent[i]) / total for i in range(len(name))]
44 |
45 | # 1회차 뽑기
46 | today_speaker = random.choices(name, speak, k=1)[0]
47 | today_wikier = random.choices(name, wiki, k=1)[0]
48 |
49 | # 오늘 결석자면 제외, 발표자와 서기가 겹치는것도 제외
50 | # choices는 리스트를 반환하며 k=1은 한명만 뽑겠다는 뜻
51 | while today_speaker in today_absent:
52 | today_speaker = random.choices(name, wiki, k=1)[0]
53 |
54 | while today_wikier in today_absent or today_wikier == today_speaker:
55 | today_wikier = random.choices(name, wiki, k=1)[0]
56 |
57 | print("<< 축 하 합 니 다 >>")
58 | print("발표자:\t", today_speaker)
59 | print("서기:\t", today_wikier)
60 | print("<< 축 하 합 니 다 >>")
61 |
62 |
63 | if __name__ == '__main__':
64 | pickme()
65 |
--------------------------------------------------------------------------------
/pickme/hyesun03/requirements.txt:
--------------------------------------------------------------------------------
1 | Click==7.0
2 |
--------------------------------------------------------------------------------
/pickme/hyesun03/study_member.json:
--------------------------------------------------------------------------------
1 | {
2 | "members": [
3 | {
4 | "name": "최혜선",
5 | "speak": [
6 | "2019-09-01"
7 | ],
8 | "wiki": [
9 | "2019-08-25"
10 | ],
11 | "absent": [
12 | "2019-09-08",
13 | "2019-09-15",
14 | "2019-09-22"
15 | ]
16 | },
17 | {
18 | "name": "강지훈",
19 | "speak": [
20 | ],
21 | "wiki": [
22 | "2019-09-01",
23 | "2019-09-08"
24 | ],
25 | "absent": [
26 | ]
27 | },
28 | {
29 | "name": "김희라",
30 | "speak": [
31 | "2019-08-25"
32 | ],
33 | "wiki": [
34 | ],
35 | "absent": [
36 | ]
37 | },
38 | {
39 | "name": "이건우",
40 | "speak": [
41 | "2019-09-08",
42 | "2019-09-15",
43 | "2019-09-22"
44 | ],
45 | "wiki": [
46 | ],
47 | "absent": [
48 | ]
49 | },
50 | {
51 | "name": "이사빈",
52 | "speak": [
53 | ],
54 | "wiki": [
55 | "2019-09-15"
56 | ],
57 | "absent": [
58 | ]
59 | },
60 | {
61 | "name": "이재란",
62 | "speak": [
63 | ],
64 | "wiki": [
65 | "2019-09-22"
66 | ],
67 | "absent": [
68 | ]
69 | },
70 | {
71 | "name": "김일범",
72 | "speak": [
73 | ],
74 | "wiki": [
75 | ],
76 | "absent": [
77 | ]
78 | },
79 | {
80 | "name": "윤지수",
81 | "speak": [
82 | ],
83 | "wiki": [
84 | ],
85 | "absent": [
86 | ]
87 | }
88 | ]
89 | }
--------------------------------------------------------------------------------
/pickme/hyesun03/test.py:
--------------------------------------------------------------------------------
1 | import json
2 | import random
3 |
4 |
5 | with open("study_member.json") as file:
6 | data = json.load(file)
7 | study_member = data["members"]
8 |
9 | name, speak, wiki, absent = [], [], [], []
10 | for member in study_member:
11 | name.append(member["name"])
12 | speak.append(len(member["speak"]))
13 | wiki.append(len(member["wiki"]))
14 | absent.append(len(member["absent"]))
15 |
16 | total = len(speak)
17 |
18 | speak = [1 - (speak[i] - absent[i]) / total for i in range(8)]
19 | wiki = [1 - (wiki[i] - absent[i]) / total for i in range(8)]
20 |
21 | today_absent = ["윤지수"]
22 |
23 | test1, test2 = {}, {}
24 | for _ in range(100000):
25 | today_speaker = random.choices(name, speak, k=1)[0]
26 | today_wikier = random.choices(name, wiki, k=1)[0]
27 |
28 | while today_speaker in today_absent:
29 | today_speaker = random.choices(name, wiki, k=1)[0]
30 |
31 | while today_wikier in today_absent or today_wikier == today_speaker:
32 | today_wikier = random.choices(name, wiki, k=1)[0]
33 |
34 | if today_speaker in test1:
35 | test1[today_speaker] += 1
36 | else:
37 | test1[today_speaker] = 1
38 |
39 | if today_wikier in test2:
40 | test2[today_wikier] += 1
41 | else:
42 | test2[today_wikier] = 1
43 |
44 | # 많이 당첨이 되지 않았거나, 많이 결석하면 더 많이 걸리는 경향이 보임
45 | print("발표", test1)
46 | print("서기", test2)
47 |
--------------------------------------------------------------------------------
/pickme/jisoo/index.html:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/pickme/jisoo/spec.md:
--------------------------------------------------------------------------------
1 | ## Summary
2 | 당일 스터디 발표를 진행할 두명 (1발표자, 1서기)을 '랜덤'하게 뽑습니다.
3 | 최근 발표 혹은 누적 발표횟수에 따라 가중치를 부여하여 '랜덤'하게 뽑습니다.
4 | 발표와 서기는 별개로 누적되어야 합니다.
5 |
6 | ## Spec
7 |
8 | 1. 최소한의 설정으로 실행가능해야 함
9 | 2. 최근 발표/작성 일에 따라 가중치를 다르게 랜덤하게 뽑아야 함.
10 | 3. 결석자에 대한 적절한 처리가 필요함.
11 | 4. 테스트를 통해 '랜덤' 하게 선출된다는 검증이 필요함.
12 |
--------------------------------------------------------------------------------