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.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"] +}