diff --git a/README.md b/README.md
index e7140d8..76c4b29 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
# FEDC5_LearningTS_Study
+
[러닝 타입스크립트](https://www.yes24.com/Product/Goods/116585556)로 타입스크립트 뿌시기 !
-
## ⭐️ 스타디 팀 ⭐️
> 조익준 김석주 이예진 백윤서 손호민 최용재 현세인
@@ -12,15 +12,16 @@
## ⏰ 스터디 시간
-**매주 월요일 10:00**
+~~매주 월요일 10:00~~
-
+**월요일 16:00**
+
## 🔨 진행 방식
- 정해진 챕터의 내용을 공부하고 본인 폴더에 정리.md 올리기
- - 안올리면 반성문
+ - 안올리면 반성문
- 지정 문제 풀고 코드파일도 올리기
- 모르는 것 자유롭게 많이 많이 질문하기
- 다른 팀원 코드 리뷰하기
@@ -30,18 +31,18 @@
## ⚙️ 컨벤션
-* 본인 이름의 브랜치에 정리 파일 및 실습 파일 올리고 PR 보내기
-* 파일명 규칙
- * 본인 이름 폴더 / 챕터 {번호 및 제목} / {정리}.md
- * 본인 이름 폴더 / 챕터 {번호 및 제목} / Practice / {문제}.ts
+- 본인 이름의 브랜치에 정리 파일 및 실습 파일 올리고 PR 보내기
+- 파일업로드
+ - `본인 이름 폴더` / 내에 정리 md 파일, 예제문제 풀이 ts파일
-
## 🗓 스터디 일정
-| 회차 | 일시 | 스터디 내용 |
-| ---- | -------- | -------- |
-| 1 | 23.11.13 월 | CHAPTER 1, 2, 3, 4 |
-
-
+| 회차 | 일시 | 스터디 내용 |
+| ---- | ----------- | -------------------------------------------------- |
+| 1 | 23.11.13 월 | CHAPTER 1, 2, 3, 4 |
+| 2 | 23.11.20 월 | CHAPTER 5, 6, 7 ,10 |
+| 3 | 23.11.27 월 | CHAPTER 8,9 + type-challenges 워밍업(1) & 쉬움(13) |
+| 4 | 23.12.04 월 | CHAPTER 15, Vue+TS 과제 중 이슈 공유 |
+| 5 | 23.12.11 월 | TodoList 과제 TS 전환 |
diff --git a/Todo-ts/.gitignore b/Todo-ts/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/Todo-ts/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/Todo-ts/index.html b/Todo-ts/index.html
new file mode 100644
index 0000000..8cc6b22
--- /dev/null
+++ b/Todo-ts/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ To Do List TS
+
+
+
+
+
+
diff --git a/Todo-ts/main.css b/Todo-ts/main.css
new file mode 100644
index 0000000..263075d
--- /dev/null
+++ b/Todo-ts/main.css
@@ -0,0 +1,143 @@
+/* 전역 css */
+body {
+ font-size: 62.5%;
+ background-color: #fff2d8;
+ margin: 0;
+ padding: 10px;
+}
+
+main {
+ background-color: #ead7bb;
+ margin: 0 auto;
+ padding: 20px;
+ width: 90vw;
+ height: 90vh;
+}
+
+/* 제목 */
+h1 {
+ font-size: 2rem;
+ text-align: center;
+ margin: 10px 0;
+ user-select: none;
+}
+
+/* 입력 폼 */
+.todoForm {
+ display: flex;
+ min-width: 400px;
+ width: 80%;
+ height: 10%;
+ text-align: center;
+ vertical-align: middle;
+ margin: 0 auto;
+ background: #ead7bb;
+ justify-content: center;
+ align-items: center;
+}
+
+.todoForm > input {
+ margin: 5px auto;
+ padding: 10px;
+ width: 80%;
+ height: 70%;
+ font-size: 1.4rem;
+ font-weight: 700;
+ border: 0;
+ border-radius: 5px;
+ vertical-align: middle;
+ box-sizing: border-box;
+}
+
+.todoForm > button {
+ margin: 5px auto;
+ text-align: center;
+ background: #113946;
+ color: white;
+ font-size: 1.2rem;
+ font-weight: 700;
+ width: 15%;
+ height: 75%;
+ border: 0;
+ border-radius: 5px;
+ cursor: pointer;
+ box-sizing: border-box;
+ user-select: none;
+}
+
+.todoForm > button:hover {
+ background: #267b97;
+}
+
+/* TodoList */
+.todoList {
+ width: 80%;
+ margin: 10px auto;
+}
+
+.todoList > ul {
+ display: flex;
+ margin: 0 auto;
+ padding: 0;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.todoList li {
+ display: flex;
+ list-style: none;
+ margin: 10px 0;
+ padding: 10px;
+ width: 100%;
+ text-align: center;
+ background: #bca37f;
+ border-radius: 5px;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+}
+
+.todoList li span {
+ margin: 0 auto;
+ font-size: 1.1rem;
+ font-weight: 700;
+ cursor: pointer;
+ user-select: none;
+}
+
+.todoList li span:hover {
+ text-decoration: underline;
+ color: #fff2d8;
+}
+
+.todoList button {
+ padding: 5px 10px;
+ background: #113946;
+ color: white;
+ font-size: 1rem;
+ font-weight: 700;
+ border: 0;
+ border-radius: 5px;
+ cursor: pointer;
+ user-select: none;
+}
+
+.todoList button:hover {
+ background: #267b97;
+}
+
+.completed {
+ text-decoration: line-through;
+ color: lightgray;
+}
+
+/* TodoCount */
+.todoCount {
+ width: 80%;
+ margin: 10px auto;
+ text-align: center;
+ font-size: 1.2rem;
+ font-weight: 700;
+ user-select: none;
+}
diff --git a/Todo-ts/package.json b/Todo-ts/package.json
new file mode 100644
index 0000000..0c30663
--- /dev/null
+++ b/Todo-ts/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "vanilla-ts",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "typescript": "^5.2.2",
+ "vite": "^5.0.0"
+ }
+}
diff --git a/Todo-ts/public/vite.svg b/Todo-ts/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/Todo-ts/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Todo-ts/src/components/App.ts b/Todo-ts/src/components/App.ts
new file mode 100644
index 0000000..3e58b4a
--- /dev/null
+++ b/Todo-ts/src/components/App.ts
@@ -0,0 +1,77 @@
+import { NewFuncParams, StateArray } from '../globalTypes';
+import { storage } from '../utils/storage';
+import validation from '../utils/validation';
+import Header from './Header';
+import TodoCount from './TodoCount';
+import TodoForm from './TodoForm';
+import TodoList from './TodoList';
+
+export default function App(
+ this: any,
+ { $target, initialState }: NewFuncParams
+) {
+ // new 미사용 방어코드
+ if (!new.target) {
+ throw new Error('new 키워드를 사용하여야 합니다.');
+ }
+ this.state = initialState;
+
+ this.setState = (nextState: StateArray) => {
+ this.state = nextState;
+ todoCount.setState(this.state);
+ todoList.setState(this.state);
+ };
+
+ // 헤더
+ new Header({
+ $target,
+ text: 'Simple Todo List',
+ });
+
+ // Todo 입력폼
+ new TodoForm({
+ $target,
+ onSubmit: (text: string) => {
+ const nextState = [
+ ...todoList.state,
+ {
+ text,
+ isCompleted: false,
+ },
+ ];
+
+ // state 유효성 검사 후 업데이트
+ this.setState(validation(nextState));
+ storage.setItem('todos', JSON.stringify(nextState));
+ },
+ });
+
+ // Todo 리스트
+ const todoList = new TodoList({
+ $target,
+ initialState: validation(initialState),
+ onClick: (text: string, id: string | undefined) => {
+ const nextState = [...this.state];
+
+ // 삭제할 값이 있을 경우 삭제
+ if (text === '삭제') nextState.splice(Number(id), 1);
+
+ nextState.forEach((task, i) => {
+ if (task.text === text && i === Number(id)) {
+ // 데이터 수정
+ nextState[i].isCompleted = !nextState[i].isCompleted;
+ }
+ });
+
+ // state 유효성 검사 후 업데이트
+ this.setState(validation(nextState));
+ storage.setItem('todos', JSON.stringify(nextState));
+ },
+ });
+
+ // TodoCount
+ const todoCount = new TodoCount({
+ $target,
+ initialState: this.state,
+ });
+}
diff --git a/Todo-ts/src/components/Header.ts b/Todo-ts/src/components/Header.ts
new file mode 100644
index 0000000..9c25d43
--- /dev/null
+++ b/Todo-ts/src/components/Header.ts
@@ -0,0 +1,21 @@
+interface HeaderParams {
+ $target: HTMLElement;
+ text: string;
+}
+
+export default function Header(this: any, { $target, text }: HeaderParams) {
+ // new 미사용 방어코드
+ if (!new.target) {
+ throw new Error('new 키워드를 사용하여야 합니다.');
+ }
+
+ const $header = document.createElement('h1');
+
+ $target.appendChild($header);
+
+ this.render = () => {
+ $header.textContent = text;
+ };
+
+ this.render();
+}
diff --git a/Todo-ts/src/components/TodoCount.ts b/Todo-ts/src/components/TodoCount.ts
new file mode 100644
index 0000000..65c19d4
--- /dev/null
+++ b/Todo-ts/src/components/TodoCount.ts
@@ -0,0 +1,40 @@
+import { StateArray, StateArrayItem } from '../globalTypes';
+import validation from '../utils/validation';
+
+interface TodoCountParams {
+ $target: HTMLElement;
+ initialState: StateArray;
+}
+
+export default function TodoCount(
+ this: any,
+ { $target, initialState }: TodoCountParams
+) {
+ // new 미사용 방어코드
+ if (!new.target) {
+ throw new Error('new 키워드를 사용하여야 합니다.');
+ }
+
+ const $todoCount = document.createElement('div');
+ $todoCount.className = 'todoCount';
+
+ $target.appendChild($todoCount);
+
+ // state 유효성 검사
+ this.state = validation(initialState);
+
+ this.render = () => {
+ const totalTodos: number = this.state.length;
+ const completedTodos: number = this.state.filter(
+ (todo: StateArrayItem) => todo.isCompleted
+ ).length;
+ $todoCount.innerHTML = `완료된 Todo의 갯수 : ${completedTodos}
전체 Todo 갯수 : ${totalTodos} `;
+ };
+
+ this.setState = (nextState: StateArray) => {
+ this.state = validation(nextState);
+ this.render();
+ };
+
+ this.render();
+}
diff --git a/Todo-ts/src/components/TodoForm.ts b/Todo-ts/src/components/TodoForm.ts
new file mode 100644
index 0000000..2b21730
--- /dev/null
+++ b/Todo-ts/src/components/TodoForm.ts
@@ -0,0 +1,52 @@
+import { QuerySelectType } from '../globalTypes';
+
+interface TodoFormParams {
+ $target: HTMLElement;
+ onSubmit: (text: string) => void;
+}
+
+export default function TodoForm(
+ this: any,
+ { $target, onSubmit }: TodoFormParams
+) {
+ // new 미사용 방어코드
+ if (!new.target) {
+ throw new Error('new 키워드를 사용하여야 합니다.');
+ }
+
+ const $form = document.createElement('form');
+ $form.className = 'todoForm';
+
+ $target.appendChild($form);
+
+ let isInit = false;
+
+ this.render = () => {
+ $form.innerHTML = `
+
+
+ `;
+
+ if (!isInit) {
+ $form.addEventListener('submit', (e) => {
+ e.preventDefault();
+ // 이건 강사님이 수정한 코드
+ const $todo: QuerySelectType =
+ $form.querySelector('input[name="todo"]');
+
+ if ($todo) {
+ const text = $todo.value;
+ // 입력값이 있을 경우만 추가
+ if (text.length > 0) {
+ $todo.value = '';
+ onSubmit(text);
+ }
+ }
+ });
+
+ isInit = true;
+ }
+ };
+
+ this.render();
+}
diff --git a/Todo-ts/src/components/TodoList.ts b/Todo-ts/src/components/TodoList.ts
new file mode 100644
index 0000000..47f590d
--- /dev/null
+++ b/Todo-ts/src/components/TodoList.ts
@@ -0,0 +1,94 @@
+// params.$target - 해당 컴포넌트가 추가가 될 DOM element
+// params.initialState - 해당 컴포넌트의 초기 상태
+
+import { StateArray } from '../globalTypes';
+import validation from '../utils/validation';
+
+interface TodoListParams {
+ $target: HTMLElement;
+ initialState: StateArray;
+ onClick: (arg1: string, arg2: string | undefined) => void;
+}
+
+export default function TodoList(
+ this: any,
+ { $target, initialState, onClick }: TodoListParams
+) {
+ // new 미사용 방어코드
+ if (!new.target) {
+ throw new Error('new 키워드를 사용하여야 합니다.');
+ }
+
+ const $todoList = document.createElement('div');
+ $todoList.className = 'todoList';
+
+ $target.appendChild($todoList);
+
+ // state 유효성 검사
+ this.state = validation(initialState);
+
+ this.setState = (nextState: StateArray) => {
+ // state 유효성 검사
+ this.state = validation(nextState);
+
+ this.render();
+ };
+
+ this.render = () => {
+ $todoList.innerHTML = `
+
+ ${this.state
+ .map(({ text, isCompleted }, i: string) => {
+ return `- ${text}
`;
+ })
+ .join('')}
+
+ `;
+
+ this.complete();
+ this.delete();
+ };
+ // todolist 삭선 기능
+ this.complete = () => {
+ const $todos = document.querySelectorAll('li');
+
+ $todos.forEach((todo) => {
+ todo.addEventListener('click', (e) => {
+ const target = e.target;
+ if (target && target instanceof HTMLElement) {
+ // 클릭된 todo의 text와 id
+ const [clickedText, clickedId] = [
+ target.innerText,
+ target.dataset.id,
+ ];
+
+ // 클릭 이벤트 내보내기
+ onClick(clickedText, clickedId);
+ }
+ });
+ });
+ };
+
+ // 삭제 버튼 기능
+ this.delete = () => {
+ // DOM element
+ const $deleteButtons = document.querySelectorAll('li > button');
+
+ $deleteButtons.forEach((button) => {
+ button.addEventListener('click', (e) => {
+ e.stopPropagation(); // 이벤트 버블링 방지
+
+ const target = e.target;
+ if (target && target instanceof HTMLElement) {
+ const [text, id] = [target.innerText, target.dataset.id];
+ // 클릭 이벤트 내보내기
+ onClick(text, id);
+ }
+ });
+ });
+ };
+
+ this.render();
+}
diff --git a/Todo-ts/src/globalTypes.ts b/Todo-ts/src/globalTypes.ts
new file mode 100644
index 0000000..1b716bd
--- /dev/null
+++ b/Todo-ts/src/globalTypes.ts
@@ -0,0 +1,13 @@
+export type QuerySelectType = T | null;
+
+export type StateArray = StateArrayItem[];
+
+export interface StateArrayItem {
+ text: string;
+ isCompleted: boolean;
+}
+
+export interface NewFuncParams {
+ $target: HTMLElement;
+ initialState: StateArray;
+}
diff --git a/Todo-ts/src/main.ts b/Todo-ts/src/main.ts
new file mode 100644
index 0000000..3c9c4fe
--- /dev/null
+++ b/Todo-ts/src/main.ts
@@ -0,0 +1,14 @@
+import App from './components/App';
+import { QuerySelectType } from './globalTypes';
+import { storage } from './utils/storage';
+import validation from './utils/validation';
+
+export const $app: QuerySelectType =
+ document.querySelector('.app');
+
+const initialState = storage.getItem('todos', []);
+
+new (App as any)({
+ $target: $app,
+ initialState: validation(initialState),
+});
diff --git a/Todo-ts/src/typescript.svg b/Todo-ts/src/typescript.svg
new file mode 100644
index 0000000..d91c910
--- /dev/null
+++ b/Todo-ts/src/typescript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Todo-ts/src/utils/storage.ts b/Todo-ts/src/utils/storage.ts
new file mode 100644
index 0000000..c620710
--- /dev/null
+++ b/Todo-ts/src/utils/storage.ts
@@ -0,0 +1,28 @@
+export const storage = (function (storage) {
+ const setItem = (key: string, value: string) => {
+ try {
+ storage.setItem(key, value);
+ } catch (e) {
+ console.log(e);
+ }
+ };
+
+ const getItem = (key: string, defaultValue: string | string[]) => {
+ try {
+ const storedValue = storage.getItem(key);
+
+ if (storedValue) {
+ return JSON.parse(storedValue);
+ }
+ return defaultValue;
+ } catch (e) {
+ console.log(e);
+ return defaultValue;
+ }
+ };
+
+ return {
+ setItem,
+ getItem,
+ };
+})(window.localStorage);
diff --git a/Todo-ts/src/utils/validation.ts b/Todo-ts/src/utils/validation.ts
new file mode 100644
index 0000000..dcf3156
--- /dev/null
+++ b/Todo-ts/src/utils/validation.ts
@@ -0,0 +1,25 @@
+import { StateArray, StateArrayItem } from '../globalTypes';
+
+export default function validation(state: StateArray) {
+ // 불가능 값
+ const invalidValue = [undefined, null, ''];
+
+ // 전체가 배열 형태인지 체크
+ if (!Array.isArray(state)) {
+ throw new Error('데이터 타입이 배열 형태가 아닙니다.');
+ }
+ // 각 요소 체크
+ state.forEach((todo: StateArrayItem) => {
+ // 객체 타입인지 체크
+ if (!(todo instanceof Object))
+ throw new Error('데이터 요소가 객체 타입이 아닙니다.');
+ // 객체 키 값이 'text'가 존재하는지 체크
+ if (invalidValue.indexOf(typeof todo.text) !== -1)
+ throw new Error('객체 키에 "text"가 없습니다.');
+ // 객체 키 값이 'isCompleted'가 존재하는지 체크
+ if (invalidValue.indexOf(typeof todo.isCompleted) !== -1)
+ throw new Error('객체 키에 "isCompleted"가 없습니다.');
+ });
+
+ return state; // 괜찮으면 반환
+}
diff --git a/Todo-ts/src/vite-env.d.ts b/Todo-ts/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/Todo-ts/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/Todo-ts/tsconfig.json b/Todo-ts/tsconfig.json
new file mode 100644
index 0000000..d897a5c
--- /dev/null
+++ b/Todo-ts/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitAny": false
+ },
+ "include": ["src"]
+}