Repository: codesquad-members-2025/FE-Todo
Pull request #22: [wanja] - 카드 생성 기능, 중복되는 코드 제거 및 가독성을 높이기 위한 리팩토링
Author: login: wan0514
Created: 2025-03-13T14:04:53Z | Merged: 2025-03-17T02:26:36Z

## 완료 작업 목록

#### **💄 스타일 변경**
- `reset.css`에 추가 스타일 적용
- `svg` 아이콘 색상 변경
  - 다양한 색상 적용을 위해 `mask-image` 스타일을 활용함

#### **✨ 기능 추가**
- **카드 폼 토글 기능 추가 및 개선**
  - "새 카드" 버튼 클릭 시 새 카드 폼을 토글할 수 있도록 구현  
  - 각 컬럼별로 독립적인 카드 폼 가시성 유지 (`+` 버튼 클릭 시 개별 폼 표시)

- **카드 추가 기능 구현**
  - 새로운 카드 등록 기능 추가  
  - `store`에서 카드 데이터를 관리하고 업데이트하는 함수 개발  
  - 카드 생성 시 UI를 동기화하여 반영하는 렌더링 함수 작성 

- **카드 삭제 기능 구현**
  -  DOM에서도 해당 카드 제거하면서 `store`의 컬럼 데이터에서도 제거하도록 로직 추가

- **UUID 기반 카드 ID 생성**
  - 각 카드가 고유한 식별자를 갖도록 UUID를 도입  

#### **♻️ 리팩토링**
- **함수명 개선**
  - 변수 및 함수명을 개선하여 가독성과 일관성 향상  

- **공통 유틸리티 함수 분리**
  - `generalUtils.js` 파일을 생성하여 공통 유틸리티 함수 관리

- **정렬 함수 리팩토링**
  - 카드 컬럼 모듈에서 사용되던 `sort` 함수를 `column.js` store로 이동하여 상태 관리 일관성 확보  
  - `columnData`를 직접 참조하도록 수정하여 불필요한 매개변수 제거  

- **중복 코드 제거 및 공통 로직 통합**

- **css 파일 분리**
  - `변수`, `font`, `svg`를 다루는 CSS 파일을 분리하여 관리함

#### **🏗 빌드 및 패키지 설치**
- `http-server` 패키지 설치 및 환경 설정  

---

## **주요 고민과 해결 과정**

### **1. 카드 생성 시 렌더링 방식 선택**  

#### **🧐 고민**  
새로 생성된 카드를 어떻게 렌더링할 것인지 두 가지 접근법을 고려함:  
1. **데이터 기반 전체 재렌더링**  
   - `store.js`의 데이터를 업데이트한 후, 이를 기반으로 **전체 카드 목록을 다시 렌더링**.  
   - ✅ 장점: 데이터와 UI가 항상 동기화됨.  
   - ❌ 단점: **모든 카드가 다시 렌더링**되므로 성능 저하 발생 가능.  
     
2. **UI 직접 조작 방식**  
   - `store.js`의 데이터를 업데이트한 후, **특정 카드만 직접 UI에 추가**.  
   - 특정 카드를 렌더링하는 함수와 **동기화 추적을 위한 별도 로직**이 필요함.  

**공통적으로 고려해야 할 점:**  
- 현재 정렬 상태(최신순/생성순)에 따라 새 카드가 삽입될 위치를 결정해야 함.  
  - 최신순(`latest`) → **맨 앞에 삽입**  
  - 생성순(`created`) → **맨 뒤에 삽입**  

#### **✅ 해결**  
- 데이터가 많아질 경우 전체 재렌더링이 **비효율적**이라 판단하여 **UI 직접 조작 방식(옵션 2번)을 선택**.  
- 정렬 상태에 따라 카드가 삽입될 위치를 결정하는 **유틸 함수**를 구현.  
- 동기화 추적을 위해 **`MutationObserver`**를 사용하여 DOM 변경 감지 후, 카드 배열의 `length`를 비교하는 방식 고려 (아직 미구현).  

---

### **2. 칸반 이벤트 위임 최적화**  

#### **🧐 고민**  
이전에는 **각 칼럼(`column-container`)에 여러 개의 이벤트 리스너를 개별적으로 추가**하고,  
각 이벤트 핸들러에서 `target.closest()`로 요소를 찾은 후, 기능을 실행하는 방식이었음.  
그러나 다음과 같은 문제점이 있었음:  
1. 이벤트 핸들러 내부에서 **너무 많은 로직을 처리** → 함수가 복잡해짐.  
2. `target.closest()`를 이벤트 핸들러마다 호출해야 해서 **중복 코드가 발생**.  
3. 새로운 기능을 추가할 때마다 **이벤트 핸들러를 수정해야 하는 불편함**.  

#### **✅ 해결**  
- **이벤트 위임(Event Delegation) 기법을 활용하여 리스너를 하나만 등록**.  
  - `column-container` 요소에 **클릭 이벤트 하나만 등록**.  
  - 클릭된 요소(`event.target.closest('button')`)의 **버튼 종류에 따라 적절한 함수를 실행**.  
- **버튼 클래스와 실행할 함수를 `Map` 객체로 관리**.  
  - 키: 버튼 클래스 이름  
  - 값: 실행할 함수  
- 이를 통해 **버튼 클래스만 `Map`에 추가하면 자동으로 분기 처리**되도록 설계하여 확장성을 높임.  

---

### **3. 하나의 파일에서 함수를 어떻게 정렬하면 보기 좋을까?**  

#### **🧐 고민**  
- 모듈 파일에서 **기능이 많아질수록 코드가 길어지고, 전체 구조를 파악하기 어려움**.  
- 함수별 **책임을 분리하여 관리하는 것이 좋지만**, 하나의 파일 안에서 **어떤 함수가 어디에 있는지 쉽게 찾을 수 있어야 함**.  

#### **✅ 해결**  
1. **주석을 사용하여 기능별로 구분**  
   - 주석을 이용해 **함수들을 카테고리별로 묶고, 가독성을 높임**.  
   - 주석 스타일을 통일하여 일관성 유지.  
   
   ```js
   // ──────────────────────────────
   //  1. 데이터 로딩 및 초기 렌더링
   // ──────────────────────────────
   function loadInitialData() { ... }
   function renderInitialUI() { ... }
   ```

2. **함수 배치 순서를 논리적인 흐름으로 정리**  
   - **데이터 → UI 렌더링 → 이벤트 핸들러 → 유틸리티 순**으로 배치
---

### **4. svg 아이콘의 색상을 어떻게 변경할 것인가?**  

#### **🧐 고민**  
- 기존에는 `<img>` 태그의 `src` 속성으로 SVG 파일을 불러와 사용함.  
  - 간결하지만 CSS로 색상 변경이 불가능함.  
- 색상이 다른 SVG 파일을 여러 개 생성할 수도 있지만, 파일 관리가 어려워지고 동적 변경 시 추가 작업이 필요함.  
- 따라서 유지보수와 확장성을 고려하여 다양한 방법을 고민함.  

#### **✅ 해결**  
- **CSS `mask-image`**를 활용하여 `<div>` 요소에 SVG를 적용하는 방식을 선택함.  
  - `background-color`로 색상을 쉽게 변경할 수 있어 재사용성이 높음.  
  - 동적으로 클래스를 추가하여 확장성이 뛰어남.  
- ❌ **단점:** `<img>` 태그를 사용할 수 없음. 시멘틱 태그에 불리함.
  - **`role="img"`과 `aria-label`을 활용하여 스크린 리더가 의미를 인식할 수 있도록 보완함.** 



Diff:
diff --git a/css/font.css b/css/font.css
new file mode 100644
index 0000000..5b71242
--- /dev/null
+++ b/css/font.css
@@ -0,0 +1,83 @@
+@import url('./variable.css');
+
+/* Typo Color */
+.text-strong {
+  color: var(--text-strong);
+}
+.text-bold {
+  color: var(--text-bold);
+}
+.text-default {
+  color: var(--text-default);
+}
+.text-weak {
+  color: var(--text-weak);
+}
+.text-brand {
+  color: var(--text-brand);
+}
+.text-danger {
+  color: var(--text-danger);
+}
+
+.text-white {
+  color: var(--text-white-default);
+}
+
+/* Typo style */
+
+/* Line Height */
+.tx-b24,
+.tx-b16,
+.tx-b14,
+.tx-b12,
+.tx-m16,
+.tx-m14,
+.tx-m12 {
+  line-height: var(--line-height-auto);
+}
+
+.tx-m16 {
+  line-height: var(--line-height-22);
+}
+
+/* 글씨 굵기 설정 */
+.tx-b24,
+.tx-b16,
+.tx-b14,
+.tx-b12,
+.tx-sb16,
+.tx-sb14,
+.tx-am16,
+.tx-am14 {
+  font-weight: var(--font-weight-bold);
+}
+
+.tx-m16,
+.tx-m14,
+.tx-m12,
+.tx-am16,
+.tx-am14 {
+  font-weight: var(--font-weight-medium);
+}
+
+/* 글씨 크기 설정 */
+.tx-b24 {
+  font-size: var(--font-size-large);
+}
+.tx-b16,
+.tx-m16,
+.tx-sb16,
+.tx-am16 {
+  font-size: var(--font-size-medium);
+}
+.tx-b14,
+.tx-m14,
+.tx-sb14,
+.tx-am14 {
+  font-size: var(--font-size-regular);
+}
+.tx-b12,
+.tx-m12 {
+  font-size: var(--font-size-small);
+}
diff --git a/css/reset.css b/css/reset.css
index 5245d65..6144b8a 100644
--- a/css/reset.css
+++ b/css/reset.css
@@ -28,3 +28,25 @@ button {
 body {
   font-family: Arial, sans-serif;
 }
+
+/* 기본 input 및 textarea 스타일 초기화 */
+input,
+textarea {
+  all: unset; /* 모든 기본 스타일 제거 */
+  box-sizing: border-box; /* 박스 크기 조정 */
+  width: 100%; /* 필요한 경우 width 설정 */
+  margin: 0; /* 기본 margin 제거 */
+  padding: 0; /* 기본 padding 제거 */
+}
+
+/* placeholder 스타일 */
+input::placeholder,
+textarea::placeholder {
+  color: #ccc; /* placeholder 텍스트 색상 */
+}
+
+/* 포커스 시 기본 outline 제거 */
+input:focus,
+textarea:focus {
+  outline: none;
+}
diff --git a/css/style.css b/css/style.css
index 718ccce..cfdf4b2 100644
--- a/css/style.css
+++ b/css/style.css
@@ -1,67 +1,6 @@
-:root {
-  /* Grayscale colors */
-  --grayscale-50: #fefefe;
-  --grayscale-100: #f7f7fc;
-  --grayscale-200: #eff0f6;
-  --grayscale-300: #d9dbe9;
-  --grayscale-400: #bec1d5;
-  --grayscale-500: #a0a3bd;
-  --grayscale-600: #6e7191;
-  --grayscale-700: #4e4b66;
-  --grayscale-800: #2a2a44;
-  --grayscale-900: #14142b;
-
-  /* Accent colors */
-  --accent-blue: #007aff;
-  --accent-red: #ff3b30;
-
-  /* Text colors */
-  --text-strong: var(--grayscale-900);
-  --text-bold: var(--grayscale-700);
-  --text-default: var(--grayscale-600);
-  --text-weak: var(--grayscale-500);
-  --text-white-default: var(--grayscale-50);
-  --text-white-weak: var(--grayscale-100);
-  --text-brand: var(--accent-blue);
-  --text-danger: var(--accent-red);
-
-  /* Surface colors */
-  --surface-default: var(--grayscale-50);
-  --surface-alt: var(--grayscale-100);
-  --surface-brand: var(--accent-blue);
-  --surface-danger: var(--accent-red);
-
-  /* Border colors */
-  --border-default: var(--grayscale-200);
-  --border-bord: var(--grayscale-400);
-
-  /* Radius */
-  --radius-small: 8px;
-  --radius-medium: 16px;
-  --radius-large: 50%;
-
-  /* Drop Shadow */
-  --drop-shadow-normal: 0 1px 4px rgba(110, 128, 145, 0.24);
-
-  --drop-shadow-up: 0 2px 8px rgba(110, 128, 145, 0.32);
-
-  --drop-shadow-floating: 0 0 4px rgba(110, 128, 145, 0.08),
-    0 16px 16px rgba(110, 128, 145, 0.24);
-
-  /* Font Size  */
-  --font-size-large: 24px;
-  --font-size-medium: 16px;
-  --font-size-regular: 14px;
-  --font-size-small: 12px;
-
-  /* Font Weights */
-  --font-weight-bold: 700;
-  --font-weight-medium: 500;
-
-  /* Line Height */
-  --line-height-auto: auto;
-  --line-height-22: 22px;
-}
+@import url('./variable.css');
+@import url('./svg.css');
+@import url('./font.css');
 
 body {
   display: flex;
@@ -74,88 +13,6 @@ body {
   background-color: var(--surface-default);
 }
 
-/* Typo Color */
-.text-strong {
-  color: var(--text-strong);
-}
-.text-bold {
-  color: var(--text-bold);
-}
-.text-default {
-  color: var(--text-default);
-}
-.text-weak {
-  color: var(--text-weak);
-}
-.text-brand {
-  color: var(--text-brand);
-}
-.text-danger {
-  color: var(--text-danger);
-}
-
-.text-white {
-  color: var(--text-white-default);
-}
-
-/* Typo style */
-
-/* Line Height */
-.tx-b24,
-.tx-b16,
-.tx-b14,
-.tx-b12,
-.tx-m16,
-.tx-m14,
-.tx-m12 {
-  line-height: var(--line-height-auto);
-}
-
-.tx-m16 {
-  line-height: var(--line-height-22);
-}
-
-/* 글씨 굵기 설정 */
-.tx-b24,
-.tx-b16,
-.tx-b14,
-.tx-b12,
-.tx-sb16,
-.tx-sb14,
-.tx-am16,
-.tx-am14 {
-  font-weight: var(--font-weight-bold);
-}
-
-.tx-m16,
-.tx-m14,
-.tx-m12,
-.tx-am16,
-.tx-am14 {
-  font-weight: var(--font-weight-medium);
-}
-
-/* 글씨 크기 설정 */
-.tx-b24 {
-  font-size: var(--font-size-large);
-}
-.tx-b16,
-.tx-m16,
-.tx-sb16,
-.tx-am16 {
-  font-size: var(--font-size-medium);
-}
-.tx-b14,
-.tx-m14,
-.tx-sb14,
-.tx-am14 {
-  font-size: var(--font-size-regular);
-}
-.tx-b12,
-.tx-m12 {
-  font-size: var(--font-size-small);
-}
-
 /* Common Structure styles */
 .flex-col {
   display: flex;
@@ -216,12 +73,11 @@ body {
   gap: 8px;
 }
 
-.card-list {
+.card-container {
   gap: 10px;
 }
 
-.todo-card {
-  display: flex;
+.card-common {
   gap: 16px;
   padding: 16px;
   background-color: var(--surface-default);
@@ -229,6 +85,55 @@ body {
   box-shadow: var(--drop-shadow-normal);
 }
 
+.todo-card {
+  display: flex;
+}
+
+.card-form {
+  margin-bottom: 10px;
+}
+
+.card-form .button-section {
+  display: flex;
+  gap: 8px;
+}
+
+.form-input::placeholder {
+  color: var(--text-strong);
+}
+
+.form-textarea {
+  height: 20px;
+  overflow-y: hidden;
+}
+
+.form-textarea::placeholder {
+  color: var(--text-default);
+}
+
+.form-common-btn {
+  height: 32px;
+  display: flex;
+  padding: 7.5px 53.5px;
+  background-color: var(--surface-alt);
+  border-radius: var(--radius-small);
+}
+
+.form-create-btn {
+  background-color: var(--surface-brand);
+}
+
+.card-form .button-section span {
+  padding: 0 4px;
+}
+
+.card-form .button-section {
+  width: 288px;
+  display: flex;
+  justify-content: space-between;
+  gap: 8px;
+}
+
 .content-section {
   width: 100%;
   gap: 8px;
@@ -259,7 +164,7 @@ body {
   border-radius: var(--radius-small);
 }
 
-.sort-btn-name {
+.sort-btn-label {
   padding: 1.5px 4px;
 }
 
@@ -276,6 +181,8 @@ body {
   border-radius: var(--radius-medium);
   background-color: var(--surface-default);
   box-shadow: var(--drop-shadow-floating);
+
+  z-index: 10;
 }
 
 #history header {
diff --git a/css/svg.css b/css/svg.css
new file mode 100644
index 0000000..0989baf
--- /dev/null
+++ b/css/svg.css
@@ -0,0 +1,50 @@
+@import url('./variable.css');
+
+.svg-24 {
+  width: 24px;
+  height: 24px;
+  -webkit-mask-size: cover;
+  mask-size: cover;
+  mask-repeat: no-repeat;
+}
+
+.svg-16 {
+  width: 16px;
+  height: 16px;
+  -webkit-mask-size: cover;
+  mask-size: cover;
+  mask-repeat: no-repeat;
+}
+
+.svg-color-weak {
+  background-color: var(--text-weak);
+}
+
+.svg-color-default {
+  background-color: var(--text-default);
+}
+
+.plus-icon {
+  -webkit-mask-image: url('/assets/icons/plus.svg');
+  mask-image: url('/assets/icons/plus.svg');
+}
+
+.closed-icon {
+  -webkit-mask-image: url('/assets/icons/closed.svg');
+  mask-image: url('/assets/icons/closed.svg');
+}
+
+.edit-icon {
+  -webkit-mask-image: url('/assets/icons/edit.svg');
+  mask-image: url('/assets/icons/edit.svg');
+}
+
+.arrow-icon {
+  -webkit-mask-image: url('/assets/icons/arrowBoth.svg');
+  mask-image: url('/assets/icons/arrowBoth.svg');
+}
+
+.history-icon {
+  -webkit-mask-image: url('/assets/icons/history.svg');
+  mask-image: url('/assets/icons/history.svg');
+}
diff --git a/css/variable.css b/css/variable.css
new file mode 100644
index 0000000..3647709
--- /dev/null
+++ b/css/variable.css
@@ -0,0 +1,64 @@
+:root {
+  /* Grayscale colors */
+  --grayscale-50: #fefefe;
+  --grayscale-100: #f7f7fc;
+  --grayscale-200: #eff0f6;
+  --grayscale-300: #d9dbe9;
+  --grayscale-400: #bec1d5;
+  --grayscale-500: #a0a3bd;
+  --grayscale-600: #6e7191;
+  --grayscale-700: #4e4b66;
+  --grayscale-800: #2a2a44;
+  --grayscale-900: #14142b;
+
+  /* Accent colors */
+  --accent-blue: #007aff;
+  --accent-red: #ff3b30;
+
+  /* Text colors */
+  --text-strong: var(--grayscale-900);
+  --text-bold: var(--grayscale-700);
+  --text-default: var(--grayscale-600);
+  --text-weak: var(--grayscale-500);
+  --text-white-default: var(--grayscale-50);
+  --text-white-weak: var(--grayscale-100);
+  --text-brand: var(--accent-blue);
+  --text-danger: var(--accent-red);
+
+  /* Surface colors */
+  --surface-default: var(--grayscale-50);
+  --surface-alt: var(--grayscale-100);
+  --surface-brand: var(--accent-blue);
+  --surface-danger: var(--accent-red);
+
+  /* Border colors */
+  --border-default: var(--grayscale-200);
+  --border-bord: var(--grayscale-400);
+
+  /* Radius */
+  --radius-small: 8px;
+  --radius-medium: 16px;
+  --radius-large: 50%;
+
+  /* Drop Shadow */
+  --drop-shadow-normal: 0 1px 4px rgba(110, 128, 145, 0.24);
+
+  --drop-shadow-up: 0 2px 8px rgba(110, 128, 145, 0.32);
+
+  --drop-shadow-floating: 0 0 4px rgba(110, 128, 145, 0.08),
+    0 16px 16px rgba(110, 128, 145, 0.24);
+
+  /* Font Size  */
+  --font-size-large: 24px;
+  --font-size-medium: 16px;
+  --font-size-regular: 14px;
+  --font-size-small: 12px;
+
+  /* Font Weights */
+  --font-weight-bold: 700;
+  --font-weight-medium: 500;
+
+  /* Line Height */
+  --line-height-auto: auto;
+  --line-height-22: 22px;
+}
diff --git a/index.html b/index.html
index 85a5b29..a62d0bd 100644
--- a/index.html
+++ b/index.html
@@ -4,6 +4,7 @@
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>TASKIFY</title>
+    <link rel="icon" href="favicon.ico" type="image/x-icon" />
     <link
       href="https://cdn.jsdelivr.net/npm/@pretendard/core/css/pretendard.css"
       rel="stylesheet"
@@ -23,24 +24,32 @@ <h1 class="text-strong tx-b24">TASKIFY</h1>
             aria-label="Sort tasks"
             data-type="created"
           >
-            <img
-              src="/assets/icons/arrowBoth.svg"
-              alt="Sort tasks"
-              width="16"
-              height="16"
-            />
-            <span class="sort-btn-name text-bold tx-m12">생성순</span>
+            <div
+              class="arrow-icon svg-16 svg-color-default"
+              role="img"
+              aria-label="View task history"
+            ></div>
+            <span class="sort-btn-label text-bold tx-m12">생성순</span>
           </button>
         </div>
         <button id="history-open-btn" aria-label="View history">
-          <img src="./assets/icons/history.svg" alt="View task history" />
+          <div
+            class="history-icon svg-24 svg-color-default"
+            role="img"
+            aria-label="View task history"
+          ></div>
         </button>
       </header>
       <aside id="history" class="flex-col">
         <header>
           <span class="text-strong tx-b16">사용자 활동 기록</span>
           <button id="history-close-btn" class="history-close-btn">
-            <img src="/assets/icons/closed.svg" width="16" height="16" />
+            <div
+              class="closed-icon svg-16 svg-color-default"
+              role="img"
+              aria-label="Add a new task"
+            ></div>
+            <!-- <img src="/assets/icons/closed.svg" width="16" height="16" /> -->
             <span class="text-default tx-b14">닫기</span>
           </button>
         </header>
diff --git a/js/components/cardColumn.js b/js/components/cardColumn.js
index 1de1559..b901956 100644
--- a/js/components/cardColumn.js
+++ b/js/components/cardColumn.js
@@ -1,115 +1,257 @@
 import { createColumn, createTaskCard } from './template.js';
-import { addChild } from '../utils/dom.js';
-import { openCardDeleteModal } from './modal.js';
-import { loadColumnData } from '../../store/column.js';
+import { pushChild, unshiftChild } from '../utils/dom.js';
+import { getISOStringNow, generateUUID } from '../utils/generalUtils.js';
+import { openDeleteCardModal } from './modal.js';
+import {
+  loadColumnsData,
+  updateCard,
+  removeCard,
+  getSortedCardsByDate,
+} from '../../store/column.js';
 
-// 전체 칼럼 생성: 초기 랜더링시
-function renderColumns(columnList) {
+// ──────────────────────────────
+//  1. 데이터 로딩 및 초기 렌더링
+// ──────────────────────────────
+
+// 전체 칼럼과 카드 렌더링
+async function initColumnAndCard() {
+  const columnsData = await loadColumnsData();
+
+  renderColumns(columnsData);
+  renderCardsForColumn(columnsData);
+
+  initKanbanEvents();
+  initSortButton();
+}
+
+// 전체 칼럼 생성
+function renderColumns(columnsData) {
   const columnContainer = document.querySelector('#columns-container');
 
-  const columnsHtml = columnList.reduce(
+  const columnsHtml = columnsData.reduce(
     (acc, { id, title, taskCount }) =>
       (acc += createColumn(id, title, taskCount)),
     ''
   );
 
-  addChild(columnContainer, columnsHtml);
+  pushChild(columnContainer, columnsHtml);
 }
 
 // 칼럼에 카드 생성
-function renderCardsForColumn(columnList) {
-  columnList.forEach(({ id, tasks }) => {
-    const columnCardList = document.querySelector(`#${id} .card-list`);
+function renderCardsForColumn(columnsData) {
+  columnsData.forEach(({ id, tasks }) => {
+    const columnCardList = document.querySelector(`#${id} .card-container`);
+
     const taskCardsHtml = tasks.reduce(
       (acc, { id, title, content, author }) =>
         (acc += createTaskCard(id, title, content, author)),
       ''
     );
 
-    addChild(columnCardList, taskCardsHtml);
+    pushChild(columnCardList, taskCardsHtml);
   });
 }
 
-// 칼럼과 카드 렌더링을 호출하는 메인 함수
-function renderColumnsAndCards(columnList) {
-  renderColumns(columnList); // 칼럼 생성
-  renderCardsForColumn(columnList); // 카드 추가
+// 칼럼에 카드 하나 생성
+function renderCard(columnId, cardData) {
+  const { id, title, content, author } = cardData;
+  const column = document.querySelector(`#${columnId} .card-container`);
+  const taskCardHtml = createTaskCard(id, title, content, author);
+
+  if (isSortCreated()) {
+    pushChild(column, taskCardHtml);
+  } else {
+    unshiftChild(column, taskCardHtml);
+  }
+}
+
+// 생성순 정렬 여부 반환
+function isSortCreated() {
+  const sortButtonLabel = document.querySelector('#sort-btn').dataset.type;
+  return sortButtonLabel === 'created';
 }
 
-// 각 칼럼의 카드 전부 제거
-function clearCards() {
-  document.querySelectorAll('.column').forEach(clearCardOfColumn);
+// ──────────────────────────────
+//  2. 카드 추가 및 삭제
+// ──────────────────────────────
 
-  function clearCardOfColumn(columnElement) {
-    const columnCardList = columnElement.querySelector('.card-list');
-    if (columnCardList) {
-      columnCardList.innerHTML = ''; // 각 칼럼의 카드 리스트만 비우기
-    }
-  }
+// 새 카드 생성
+function createNewCard(target) {
+  const column = getColumn(target);
+  if (!column) return;
+
+  const formCard = getCardForm(column);
+  if (!formCard) return;
+
+  const inputData = collectInputData(formCard);
+  if (!inputData) return;
+
+  closeCardForm(formCard);
+  saveAndRenderCard(column.id, inputData);
+  resetValues(formCard);
+}
+
+// 카드 데이터 저장 & UI 업데이트
+function saveAndRenderCard(columnId, inputData) {
+  updateCard(columnId, inputData);
+  renderCard(columnId, inputData);
+}
+
+// 입력 값 초기화
+function resetValues(formCard) {
+  formCard.querySelector('input').value = '';
+  formCard.querySelector('textarea').value = '';
 }
 
-// 카드 제거
+// 카드 제거 함수 반환 (고차함수)
 function makeCardRemover(cardId) {
-  return function () {
+  return () => {
     const targetCard = document.getElementById(cardId);
-    targetCard.remove();
+    const column = targetCard.closest('.column');
+    const columnId = column.id;
+
+    removeCard(columnId, cardId); // 데이터 제거
+    targetCard.remove(); // UI 제거
   };
 }
 
-//카드 제거 버튼 이벤트 추가 함수
-function initCardRemoveButton() {
-  const columnSection = document.getElementById('columns-container');
-  columnSection.addEventListener('click', openCardDeleteModal);
+// 각 칼럼의 카드 전부 제거
+function clearCards() {
+  document.querySelectorAll('.column').forEach(clearCardOfColumn);
+}
+
+// 특정 칼럼의 카드 제거
+function clearCardOfColumn(columnElement) {
+  const columnCardList = columnElement.querySelector('.card-container');
+  if (columnCardList) {
+    columnCardList.innerHTML = ''; // 카드 리스트 비우기
+  }
 }
 
-//정렬 버튼 클릭 이벤트
-function sortCards(columnList, { currentTarget }) {
+// ──────────────────────────────
+//  3. 카드 정렬 기능
+// ──────────────────────────────
+
+// 정렬 버튼 클릭 이벤트
+async function sortCards({ currentTarget }) {
   const sortButton = currentTarget;
-  const sortButtonName = sortButton.querySelector('.sort-btn-name');
+  const sortButtonLabel = sortButton.querySelector('.sort-btn-label');
   const currentSortType = sortButton.dataset.type;
 
+  const { newSortType, buttonText } = toggleSortType(currentSortType);
+
+  updateSortButtonUI(sortButton, sortButtonLabel, newSortType, buttonText);
+  applySortedCards(newSortType);
+}
+
+// 정렬 타입 변경
+function toggleSortType(currentSortType) {
   const newSortType = currentSortType === 'created' ? 'latest' : 'created';
   const buttonText = newSortType === 'created' ? '생성순' : '최신순';
+  return { newSortType, buttonText };
+}
 
-  // 상태 변경 및 버튼 텍스트 업데이트
-  sortButton.dataset.type = newSortType;
-  sortButtonName.textContent = buttonText;
-
-  // 데이터 정렬
-  const sortedData = getSortedTasksByDate(columnList, newSortType);
+// 버튼 UI 업데이트
+function updateSortButtonUI(button, label, newSortType, buttonText) {
+  button.dataset.type = newSortType;
+  label.textContent = buttonText;
+}
 
-  // 카드 업데이트
+// 정렬 후 UI 반영
+function applySortedCards(sortType) {
+  const sortedData = getSortedCardsByDate(sortType);
   clearCards();
   renderCardsForColumn(sortedData);
 }
 
-// 정렬 함수 (order: 'created' | 'latest')
-function getSortedTasksByDate(columnList, order = 'created') {
-  return columnList.map((column) => ({
-    ...column,
-    tasks: [...column.tasks].sort((a, b) => {
-      return order === 'created'
-        ? new Date(a.createdAt) - new Date(b.createdAt) // 생성순
-        : new Date(b.createdAt) - new Date(a.createdAt); // 최신순
-    }),
-  }));
+// ──────────────────────────────
+//  4. 이벤트 핸들링
+// ──────────────────────────────
+
+// Kanban 이벤트 위임
+function initKanbanEvents() {
+  const columnSection = document.getElementById('columns-container');
+
+  const clickHandlers = new Map([
+    ['.card-add-btn', toggleCardForm],
+    ['.delete-card-btn', openDeleteCardModal],
+    ['.form-cancel-btn', toggleCardForm],
+    ['.form-create-btn', createNewCard],
+  ]);
+
+  columnSection.addEventListener('click', ({ target }) => {
+    const closestBtn = target.closest('button');
+    if (closestBtn === null) return; // 다른 곳 누르면 실행 X
+
+    for (const [selector, handler] of clickHandlers) {
+      if (closestBtn.matches(selector)) {
+        handler(closestBtn);
+        break;
+      }
+    }
+  });
 }
 
-//Sort Button
-function initSortButton(columnData) {
+// Sort Button 이벤트 초기화
+function initSortButton() {
   const sortButton = document.getElementById('sort-btn');
-  sortButton.addEventListener('click', (event) => {
-    sortCards(columnData, event);
-  });
+  sortButton.addEventListener('click', sortCards);
 }
 
-async function initColumnAndCard() {
-  const data = await loadColumnData();
+// ──────────────────────────────
+//  5. 카드 폼 관련 기능
+// ──────────────────────────────
+
+// 카드 폼 토글
+function toggleCardForm(target) {
+  const column = getColumn(target);
+  const cardForm = getCardForm(column);
+  if (!cardForm) return;
+
+  const isVisible = window.getComputedStyle(cardForm).display === 'flex';
+  cardForm.style.display = isVisible ? 'none' : 'flex';
+}
+
+// 칼럼 요소 가져오기
+function getColumn(target) {
+  return target.closest('.column');
+}
+
+// 카드 폼 가져오기
+function getCardForm(column) {
+  return column?.querySelector('.card-form') || null;
+}
 
-  renderColumnsAndCards(data);
+// 입력 데이터 수집 및 검증
+function collectInputData(formCard) {
+  const input = formCard.querySelector('input');
+  const textarea = formCard.querySelector('textarea');
+
+  const title = input?.value;
+  const content = textarea?.value;
+
+  if (!title || !content) return null; // 입력값이 없으면 null 반환
+
+  return {
+    id: generateUUID(),
+    createdAt: getISOStringNow(),
+    author: detectDeviceType(),
+    title,
+    content,
+  };
+}
+
+// 디바이스 감지
+function detectDeviceType() {
+  return navigator.maxTouchPoints > 0 ||
+    /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent)
+    ? 'mobile'
+    : 'web';
+}
 
-  initCardRemoveButton();
-  initSortButton(data);
+// 입력 폼 닫기
+function closeCardForm(formCard) {
+  formCard.style.display = 'none';
 }
 
 export { clearCards, makeCardRemover, initColumnAndCard };
diff --git a/js/components/history.js b/js/components/history.js
index 396ca5b..15fffa9 100644
--- a/js/components/history.js
+++ b/js/components/history.js
@@ -1,73 +1,97 @@
 import { createHistoryItem } from './template.js';
-import { addChild } from '../utils/dom.js';
+import { pushChild } from '../utils/dom.js';
 import { openHistoryDeleteModal } from './modal.js';
 import { loadHistoryData } from '../../store/history.js';
 
+// ──────────────────────────────
+//  1. 캐싱된 DOM 요소(동적이 요소가 아닌 DOM 요소)
+// ──────────────────────────────
+const historyContainer = document.getElementById('history-container');
+const historyDefault = document.getElementById('history-default');
+const historyFooter = document.querySelector('#history footer');
+const historySection = document.getElementById('history');
+
+// ──────────────────────────────
+//  2. 기록 렌더링 및 삭제
+// ──────────────────────────────
+
+// 기록 렌더링
 function renderHistoryItems(historyList) {
   if (historyList.length === 0) {
-    toggleHistoryDefaultUi(true);
+    setHistoryDefaultUI(true);
     return;
   }
-  const historyContainer = document.querySelector('#history-container');
 
   const historyItemsHtml = historyList.reduce(
-    (acc, { username, profileImage, actionText, timestamp }) => {
+    (items, { username, profileImage, actionText, timestamp }) => {
       return (
-        acc + createHistoryItem(username, profileImage, actionText, timestamp)
+        items + createHistoryItem(username, profileImage, actionText, timestamp)
       );
     },
     ''
   );
 
-  addChild(historyContainer, historyItemsHtml);
-  toggleDeleteButton(true); // 기록삭제 버튼 보여주기
+  pushChild(historyContainer, historyItemsHtml);
+  setHistoryDeleteButton(true);
 }
 
-// Remove history record
+// 기록 삭제
 function removeHistoryRecords() {
-  document.getElementById('history-container').innerHTML = '';
-  toggleHistoryDefaultUi(true);
-  toggleDeleteButton(false); // 기록삭제 버튼 감추기
+  historyContainer.innerHTML = '';
+  setHistoryDefaultUI(true);
+  setHistoryDeleteButton(false);
 }
 
-//history 열기
+// ──────────────────────────────
+//  3. UI 토글 함수
+// ──────────────────────────────
+
+// 히스토리 열기/닫기
 function toggleHistory() {
-  const history = document.querySelector('#history');
-  const isVisable = window.getComputedStyle(history).display === 'flex';
+  setHistoryVisibility(historySection.style.display !== 'flex');
+}
 
-  history.style.display = isVisable ? 'none' : 'flex';
+// 히스토리 표시 여부 설정
+function setHistoryVisibility(isVisible) {
+  historySection.style.display = isVisible ? 'flex' : 'none';
 }
 
-// Toggle Default history
-function toggleHistoryDefaultUi(show) {
-  const historyDefault = document.getElementById('history-default');
-  historyDefault.style.display = show ? 'flex' : 'none';
+// 기본 UI 표시 여부
+function setHistoryDefaultUI(isVisible) {
+  historyDefault.style.display = isVisible ? 'flex' : 'none';
 }
 
-// Toggle History Footer
-function toggleDeleteButton(show) {
-  const footerElement = document.querySelector('#history footer');
-  footerElement.style.display = show ? 'flex' : 'none';
+// 기록 삭제 버튼 표시 여부
+function setHistoryDeleteButton(isVisible) {
+  historyFooter.style.display = isVisible ? 'flex' : 'none';
 }
 
-//History Event Listener
-function initHistoryButton() {
-  const historyButton = document.getElementById('history-open-btn');
-  historyButton.addEventListener('click', toggleHistory);
+// ──────────────────────────────
+//  4. 이벤트 핸들링
+// ──────────────────────────────
 
-  const historyCloseButton = document.getElementById('history-close-btn');
-  historyCloseButton.addEventListener('click', toggleHistory);
+// 이벤트 리스너 등록
+function initHistoryButtons() {
+  const buttonMap = new Map([
+    ['#history-open-btn', toggleHistory],
+    ['#history-close-btn', toggleHistory],
+    ['#history-delete-btn', openHistoryDeleteModal],
+  ]);
 
-  const historyDeleteButton = document.getElementById('history-delete-btn');
-  historyDeleteButton.addEventListener('click', openHistoryDeleteModal);
+  buttonMap.forEach((handler, selector) => {
+    const button = document.querySelector(selector);
+    if (button) button.addEventListener('click', handler);
+  });
 }
 
+// ──────────────────────────────
+//  5. 히스토리 초기화
+// ──────────────────────────────
+
 async function initHistory() {
   const data = await loadHistoryData();
-
   renderHistoryItems(data);
-
-  initHistoryButton();
+  initHistoryButtons();
 }
 
 export { initHistory, removeHistoryRecords };
diff --git a/js/components/modal.js b/js/components/modal.js
index aa1adcb..eaab364 100644
--- a/js/components/modal.js
+++ b/js/components/modal.js
@@ -1,61 +1,74 @@
 import { makeCardRemover } from './cardColumn.js';
 import { removeHistoryRecords } from './history.js';
 
-function toggleModal() {
-  const dialog = document.getElementById('modal');
-  dialog.open ? dialog.close() : dialog.showModal();
-}
+// ──────────────────────────────
+// 1. 캐싱된 DOM 요소
+// ──────────────────────────────
+const modal = document.getElementById('modal');
+const modalDescription = modal.querySelector('#modal-description');
+const modalDeleteBtn = modal.querySelector('#modal-delete-btn');
+const modalCancelBtn = modal.querySelector('#modal-cancel-btn');
 
-function createConfirmModal(textContent, deleteCallback) {
-  const dialog = document.getElementById('modal');
-  const description = dialog.querySelector('#modal-description');
-  description.textContent = textContent;
+// ──────────────────────────────
+// 2. Modal Toggle 기능
+// ──────────────────────────────
 
-  const deleteBtn = dialog.querySelector('#modal-delete-btn');
+// 모달 열기
+function openModal() {
+  modal.showModal();
+}
 
-  // 기존 삭제 버튼 이벤트 제거
-  if (deleteBtn._deleteHandler) {
-    deleteBtn.removeEventListener('click', deleteBtn._deleteHandler);
-  }
+// 모달 닫기
+function closeModal() {
+  modal.close();
+}
 
-  // 새로운 삭제 이벤트 핸들러 등록
-  deleteBtn._deleteHandler = () => {
+function setConfirmModal(textContent, deleteCallback) {
+  modalDescription.textContent = textContent;
+  setDeleteButtonHandler(deleteCallback);
+  openModal();
+}
+
+// 삭제 버튼 핸들링 : onclick이 기존 함수를 자동으로 덮어씌워줌
+function setDeleteButtonHandler(deleteCallback) {
+  modalDeleteBtn.onclick = () => {
     deleteCallback();
-    toggleModal();
+    closeModal();
   };
-
-  deleteBtn.addEventListener('click', deleteBtn._deleteHandler);
 }
 
+// ──────────────────────────────
+// 2. 삭제 모달 핸들링
+// ──────────────────────────────
+
 // 카드 삭제 모달 오픈
-function openCardDeleteModal(event) {
-  const button = event.target.closest('.delete-card-btn');
-  if (!button) return;
-
-  // 가장 가까운 .todo-list 안에 있는 .card-list 찾기
-  const todoList = button.closest('.card-list');
-  if (!todoList) return; // todo-list 내부에서만 동작하도록 제한
-
-  const card = button.closest('.todo-card');
-  if (card) {
-    const cardRemover = makeCardRemover(card.id);
-    createConfirmModal('선택한 카드를 삭제할까요?', cardRemover);
-    toggleModal();
-  }
-}
+function openDeleteCardModal(target) {
+  const todoCard = target.closest('.todo-card');
+  if (!todoCard) return; // 카드가 없으면 종료
+
+  const cardId = todoCard.id;
+  if (!cardId) return;
 
-document.addEventListener('DOMContentLoaded', () => {
-  const cancelBtn = document.querySelector('#modal-cancel-btn');
-  if (cancelBtn) cancelBtn.addEventListener('click', toggleModal);
-});
+  const cardRemover = makeCardRemover(cardId);
+  setConfirmModal('선택한 카드를 삭제할까요?', cardRemover);
+  openModal();
+}
 
 // 활동기록 삭제 모달 오픈
 function openHistoryDeleteModal() {
-  createConfirmModal(
-    '모든 사용자 활동 기록을 삭제할까요?',
-    removeHistoryRecords
-  );
-  toggleModal();
+  setConfirmModal('모든 사용자 활동 기록을 삭제할까요?', removeHistoryRecords);
+  openModal();
 }
 
-export { openCardDeleteModal, openHistoryDeleteModal };
+// ──────────────────────────────
+// 🟢 4. 모달 초기화(취소 버튼만)
+//  TODO 삭제버튼이 동적으로 생성되기 때문에, 취소버튼을 따로 뺐으나 더 좋은 방법이 있을지 찾아보기
+// ──────────────────────────────
+
+function initModalCloseEvent() {
+  modalCancelBtn.addEventListener('click', closeModal);
+}
+
+initModalCloseEvent();
+
+export { openDeleteCardModal, openHistoryDeleteModal };
diff --git a/js/components/template.js b/js/components/template.js
index 50c4b71..7cf624d 100644
--- a/js/components/template.js
+++ b/js/components/template.js
@@ -10,36 +10,57 @@ function createColumn(id, title, taskCount) {
         </div>
       </div>
       <div class="column-area-right">
-        <button aria-label="Add task">
-          <img src="./assets/icons/plus.svg" alt="Add a new task" />
+        <button class="card-add-btn" aria-label="Add task">
+          <div class="plus-icon svg-24 svg-color-weak" role="img" aria-label="Add a new task"></div>
         </button>
         <button aria-label="Delete column">
-          <img src="./assets/icons/closed.svg" alt="Delete this column" />
+          <div class="closed-icon svg-24 svg-color-weak" role="img" aria-label="Delete this column"></div>
         </button>
       </div>
     </div>
     <div class="card-list flex-col">
-      <!-- 카드 항목은 여기서 동적으로 추가 -->
+      ${cardForm}
+      <div class="card-container flex-col">
+        <!-- 카드 항목은 여기서 동적으로 추가 -->
+      </div>
     </div>
   </section>
 `;
 }
 
+const cardForm = `
+  <article class="card-form card-common flex-col" style="display: none">
+  <div class="content-section flex-col">
+    <input type="text" class="form-input text-strong tx-m14" placeholder="제목을 입력하세요" value="">
+    <textarea class="form-textarea text-default tx-m14" placeholder="내용을 입력하세요" value=""></textarea>
+  </div>
+  <div class="button-section">
+    <button class="form-cancel-btn form-common-btn text-default tx-b14">
+      <span>취소</span>
+    </button>
+    <button class="form-create-btn form-common-btn text-white tx-b14">
+      <span>저장</span>
+    </button>
+  </div>
+</article>
+
+`;
+
 // Task 카드 컴포넌트 생성
 function createTaskCard(id, title, content, author) {
   return `
-  <article class="todo-card" id="${id}">
+  <article class="todo-card card-common" id="${id}">
     <div class="content-section flex-col">
       <h3 class="text-strong tx-b14">${title}</h3>
       <p class="text-default tx-m14">${content}</p>
       <h6 class="text-weak tx-m12">author by ${author}</h6>
     </div>
     <div class="button-section flex-col">
-      <button class= "delete-card-btn" aria-label="Delete task">
-        <img src="./assets/icons/closed.svg" alt="Delete this task" />
+      <button class="delete-card-btn" aria-label="Delete task">
+        <div class="closed-icon svg-24 svg-color-weak" role="img" aria-label="Delete this task"></div>
       </button>
-      <button aria-label="Edit task">
-        <img src="./assets/icons/edit.svg" alt="Edit this task" />
+      <button class="edit-card-btn" aria-label="Edit task">
+        <div class="edit-icon svg-24 svg-color-weak" role="img" aria-label="Edit this task"></div>
       </button>
     </div>
   </article>
diff --git a/js/main.js b/js/main.js
index 25fa9e0..e5fec84 100644
--- a/js/main.js
+++ b/js/main.js
@@ -2,5 +2,4 @@ import { initColumnAndCard } from './components/cardColumn.js';
 import { initHistory } from './components/history.js';
 
 initColumnAndCard();
-
 initHistory();
diff --git a/js/utils/dom.js b/js/utils/dom.js
index 76e56dd..f9caa47 100644
--- a/js/utils/dom.js
+++ b/js/utils/dom.js
@@ -1,5 +1,5 @@
 //dom에 요소 추가하는 함수
-function addChild(parentElement, newElement) {
+function pushChild(parentElement, newElement) {
   if (typeof newElement === 'string') {
     parentElement.insertAdjacentHTML('beforeend', newElement);
   } else {
@@ -7,4 +7,12 @@ function addChild(parentElement, newElement) {
   }
 }
 
-export { addChild };
+function unshiftChild(parentElement, newElement) {
+  if (typeof newElement === 'string') {
+    parentElement.insertAdjacentHTML('afterbegin', newElement);
+  } else {
+    parentElement.prepend(newElement);
+  }
+}
+
+export { pushChild, unshiftChild };
diff --git a/js/utils/generalUtils.js b/js/utils/generalUtils.js
new file mode 100644
index 0000000..8ff4630
--- /dev/null
+++ b/js/utils/generalUtils.js
@@ -0,0 +1,14 @@
+function getISOStringNow() {
+  const date = new Date();
+  return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
+}
+
+function generateUUID() {
+  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
+    const random = (Math.random() * 16) | 0; // 0~15 사이의 랜덤 정수
+    const value = char === 'x' ? random : (random & 0x3) | 0x8; // 'y' 자리에는 8~B(8~11) 값만 들어가도록 설정
+    return value.toString(16); // 16진수 변환
+  });
+}
+
+export { getISOStringNow, generateUUID };
diff --git a/package.json b/package.json
index 37244d3..38238b6 100644
--- a/package.json
+++ b/package.json
@@ -8,5 +8,8 @@
   },
   "author": "wan0514",
   "license": "ISC",
-  "type": "module"
+  "type": "module",
+  "dependencies": {
+    "http-server": "^14.1.1"
+  }
 }
diff --git a/store/column.js b/store/column.js
index ed3941b..9af623b 100644
--- a/store/column.js
+++ b/store/column.js
@@ -4,11 +4,33 @@ const COLUMN_DATA_URL = './data/columnData.json';
 
 let columnData = null;
 
-async function loadColumnData() {
+async function loadColumnsData() {
   if (columnData === null) {
     columnData = await fetchData(COLUMN_DATA_URL);
   }
   return columnData;
 }
 
-export { loadColumnData };
+function updateCard(columnId, cardData) {
+  const targetColumn = columnData.find((column) => column.id === columnId);
+  targetColumn.tasks.push(cardData);
+}
+
+function removeCard(columnId, cardId) {
+  const targetColumn = columnData.find((column) => column.id === columnId);
+  targetColumn.tasks = targetColumn.tasks.filter((task) => task.id !== cardId);
+}
+
+// 정렬 함수 (order: 'created' | 'latest')
+function getSortedCardsByDate(order = 'created') {
+  return columnData.map((column) => ({
+    ...column,
+    tasks: [...column.tasks].sort((a, b) => {
+      return order === 'created'
+        ? new Date(a.createdAt) - new Date(b.createdAt) // 생성순
+        : new Date(b.createdAt) - new Date(a.createdAt); // 최신순
+    }),
+  }));
+}
+
+export { loadColumnsData, updateCard, removeCard, getSortedCardsByDate };


Reviews:
user: login: crongro
body: 함수들이 간결하네요.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T02:26:28Z
user: login: wan0514
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T02:18:45Z
user: login: wan0514
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T02:51:06Z
user: login: wan0514
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T03:03:26Z

Review comments:
diff_hunk: @@ -1,115 +1,257 @@
 import { createColumn, createTaskCard } from './template.js';
-import { addChild } from '../utils/dom.js';
-import { openCardDeleteModal } from './modal.js';
-import { loadColumnData } from '../../store/column.js';
+import { pushChild, unshiftChild } from '../utils/dom.js';
+import { getISOStringNow, generateUUID } from '../utils/generalUtils.js';
+import { openDeleteCardModal } from './modal.js';
+import {
+  loadColumnsData,
+  updateCard,
+  removeCard,
+  getSortedCardsByDate,
+} from '../../store/column.js';
 
-// 전체 칼럼 생성: 초기 랜더링시
-function renderColumns(columnList) {
+// ──────────────────────────────
+//  1. 데이터 로딩 및 초기 렌더링
+// ──────────────────────────────
+
+// 전체 칼럼과 카드 렌더링
+async function initColumnAndCard() {
+  const columnsData = await loadColumnsData();
+
+  renderColumns(columnsData);
+  renderCardsForColumn(columnsData);
+
+  initKanbanEvents();
+  initSortButton();
+}
+
+// 전체 칼럼 생성
+function renderColumns(columnsData) {
   const columnContainer = document.querySelector('#columns-container');
 
-  const columnsHtml = columnList.reduce(
+  const columnsHtml = columnsData.reduce(
     (acc, { id, title, taskCount }) =>
       (acc += createColumn(id, title, taskCount)),
     ''
   );
 
-  addChild(columnContainer, columnsHtml);
+  pushChild(columnContainer, columnsHtml);
 }
 
 // 칼럼에 카드 생성
-function renderCardsForColumn(columnList) {
-  columnList.forEach(({ id, tasks }) => {
-    const columnCardList = document.querySelector(`#${id} .card-list`);
+function renderCardsForColumn(columnsData) {
+  columnsData.forEach(({ id, tasks }) => {
+    const columnCardList = document.querySelector(`#${id} .card-container`);
+
     const taskCardsHtml = tasks.reduce(
       (acc, { id, title, content, author }) =>
         (acc += createTaskCard(id, title, content, author)),
       ''
     );
 
-    addChild(columnCardList, taskCardsHtml);
+    pushChild(columnCardList, taskCardsHtml);
   });
 }
 
-// 칼럼과 카드 렌더링을 호출하는 메인 함수
-function renderColumnsAndCards(columnList) {
-  renderColumns(columnList); // 칼럼 생성
-  renderCardsForColumn(columnList); // 카드 추가
+// 칼럼에 카드 하나 생성
+function renderCard(columnId, cardData) {
+  const { id, title, content, author } = cardData;
path: js/components/cardColumn.js
user: login: crongro
body: 해체할당으로 잘 받았네요.
created_at: 2025-03-17T02:19:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,115 +1,257 @@
 import { createColumn, createTaskCard } from './template.js';
-import { addChild } from '../utils/dom.js';
-import { openCardDeleteModal } from './modal.js';
-import { loadColumnData } from '../../store/column.js';
+import { pushChild, unshiftChild } from '../utils/dom.js';
+import { getISOStringNow, generateUUID } from '../utils/generalUtils.js';
+import { openDeleteCardModal } from './modal.js';
+import {
+  loadColumnsData,
+  updateCard,
+  removeCard,
+  getSortedCardsByDate,
+} from '../../store/column.js';
 
-// 전체 칼럼 생성: 초기 랜더링시
-function renderColumns(columnList) {
+// ──────────────────────────────
+//  1. 데이터 로딩 및 초기 렌더링
+// ──────────────────────────────
+
+// 전체 칼럼과 카드 렌더링
+async function initColumnAndCard() {
+  const columnsData = await loadColumnsData();
+
+  renderColumns(columnsData);
+  renderCardsForColumn(columnsData);
+
+  initKanbanEvents();
+  initSortButton();
+}
+
+// 전체 칼럼 생성
+function renderColumns(columnsData) {
   const columnContainer = document.querySelector('#columns-container');
 
-  const columnsHtml = columnList.reduce(
+  const columnsHtml = columnsData.reduce(
     (acc, { id, title, taskCount }) =>
       (acc += createColumn(id, title, taskCount)),
     ''
   );
 
-  addChild(columnContainer, columnsHtml);
+  pushChild(columnContainer, columnsHtml);
 }
 
 // 칼럼에 카드 생성
-function renderCardsForColumn(columnList) {
-  columnList.forEach(({ id, tasks }) => {
-    const columnCardList = document.querySelector(`#${id} .card-list`);
+function renderCardsForColumn(columnsData) {
+  columnsData.forEach(({ id, tasks }) => {
+    const columnCardList = document.querySelector(`#${id} .card-container`);
+
     const taskCardsHtml = tasks.reduce(
       (acc, { id, title, content, author }) =>
         (acc += createTaskCard(id, title, content, author)),
       ''
     );
 
-    addChild(columnCardList, taskCardsHtml);
+    pushChild(columnCardList, taskCardsHtml);
   });
 }
 
-// 칼럼과 카드 렌더링을 호출하는 메인 함수
-function renderColumnsAndCards(columnList) {
-  renderColumns(columnList); // 칼럼 생성
-  renderCardsForColumn(columnList); // 카드 추가
+// 칼럼에 카드 하나 생성
+function renderCard(columnId, cardData) {
+  const { id, title, content, author } = cardData;
+  const column = document.querySelector(`#${columnId} .card-container`);
+  const taskCardHtml = createTaskCard(id, title, content, author);
+
+  if (isSortCreated()) {
+    pushChild(column, taskCardHtml);
+  } else {
+    unshiftChild(column, taskCardHtml);
+  }
path: js/components/cardColumn.js
user: login: crongro
body: 이렇게 짧으면 3항연산자 표현도 좋죠
created_at: 2025-03-17T02:19:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,115 +1,257 @@
 import { createColumn, createTaskCard } from './template.js';
-import { addChild } from '../utils/dom.js';
-import { openCardDeleteModal } from './modal.js';
-import { loadColumnData } from '../../store/column.js';
+import { pushChild, unshiftChild } from '../utils/dom.js';
+import { getISOStringNow, generateUUID } from '../utils/generalUtils.js';
+import { openDeleteCardModal } from './modal.js';
+import {
+  loadColumnsData,
+  updateCard,
+  removeCard,
+  getSortedCardsByDate,
+} from '../../store/column.js';
 
-// 전체 칼럼 생성: 초기 랜더링시
-function renderColumns(columnList) {
+// ──────────────────────────────
+//  1. 데이터 로딩 및 초기 렌더링
+// ──────────────────────────────
+
+// 전체 칼럼과 카드 렌더링
+async function initColumnAndCard() {
+  const columnsData = await loadColumnsData();
+
+  renderColumns(columnsData);
+  renderCardsForColumn(columnsData);
+
+  initKanbanEvents();
+  initSortButton();
+}
+
+// 전체 칼럼 생성
+function renderColumns(columnsData) {
   const columnContainer = document.querySelector('#columns-container');
 
-  const columnsHtml = columnList.reduce(
+  const columnsHtml = columnsData.reduce(
     (acc, { id, title, taskCount }) =>
       (acc += createColumn(id, title, taskCount)),
     ''
   );
 
-  addChild(columnContainer, columnsHtml);
+  pushChild(columnContainer, columnsHtml);
 }
 
 // 칼럼에 카드 생성
-function renderCardsForColumn(columnList) {
-  columnList.forEach(({ id, tasks }) => {
-    const columnCardList = document.querySelector(`#${id} .card-list`);
+function renderCardsForColumn(columnsData) {
+  columnsData.forEach(({ id, tasks }) => {
+    const columnCardList = document.querySelector(`#${id} .card-container`);
+
     const taskCardsHtml = tasks.reduce(
       (acc, { id, title, content, author }) =>
         (acc += createTaskCard(id, title, content, author)),
       ''
     );
 
-    addChild(columnCardList, taskCardsHtml);
+    pushChild(columnCardList, taskCardsHtml);
   });
 }
 
-// 칼럼과 카드 렌더링을 호출하는 메인 함수
-function renderColumnsAndCards(columnList) {
-  renderColumns(columnList); // 칼럼 생성
-  renderCardsForColumn(columnList); // 카드 추가
+// 칼럼에 카드 하나 생성
+function renderCard(columnId, cardData) {
+  const { id, title, content, author } = cardData;
+  const column = document.querySelector(`#${columnId} .card-container`);
+  const taskCardHtml = createTaskCard(id, title, content, author);
+
+  if (isSortCreated()) {
+    pushChild(column, taskCardHtml);
+  } else {
+    unshiftChild(column, taskCardHtml);
+  }
+}
+
+// 생성순 정렬 여부 반환
+function isSortCreated() {
+  const sortButtonLabel = document.querySelector('#sort-btn').dataset.type;
+  return sortButtonLabel === 'created';
 }
 
-// 각 칼럼의 카드 전부 제거
-function clearCards() {
-  document.querySelectorAll('.column').forEach(clearCardOfColumn);
+// ──────────────────────────────
+//  2. 카드 추가 및 삭제
+// ──────────────────────────────
 
-  function clearCardOfColumn(columnElement) {
-    const columnCardList = columnElement.querySelector('.card-list');
-    if (columnCardList) {
-      columnCardList.innerHTML = ''; // 각 칼럼의 카드 리스트만 비우기
-    }
-  }
+// 새 카드 생성
+function createNewCard(target) {
+  const column = getColumn(target);
+  if (!column) return;
+
+  const formCard = getCardForm(column);
+  if (!formCard) return;
+
+  const inputData = collectInputData(formCard);
+  if (!inputData) return;
+
+  closeCardForm(formCard);
+  saveAndRenderCard(column.id, inputData);
+  resetValues(formCard);
+}
+
+// 카드 데이터 저장 & UI 업데이트
+function saveAndRenderCard(columnId, inputData) {
+  updateCard(columnId, inputData);
+  renderCard(columnId, inputData);
+}
+
+// 입력 값 초기화
+function resetValues(formCard) {
+  formCard.querySelector('input').value = '';
+  formCard.querySelector('textarea').value = '';
 }
 
-// 카드 제거
+// 카드 제거 함수 반환 (고차함수)
 function makeCardRemover(cardId) {
-  return function () {
+  return () => {
     const targetCard = document.getElementById(cardId);
-    targetCard.remove();
+    const column = targetCard.closest('.column');
+    const columnId = column.id;
+
+    removeCard(columnId, cardId); // 데이터 제거
+    targetCard.remove(); // UI 제거
   };
 }
 
-//카드 제거 버튼 이벤트 추가 함수
-function initCardRemoveButton() {
-  const columnSection = document.getElementById('columns-container');
-  columnSection.addEventListener('click', openCardDeleteModal);
+// 각 칼럼의 카드 전부 제거
+function clearCards() {
+  document.querySelectorAll('.column').forEach(clearCardOfColumn);
+}
+
+// 특정 칼럼의 카드 제거
+function clearCardOfColumn(columnElement) {
path: js/components/cardColumn.js
user: login: crongro
body: 콜백함수 분리 잘했음.
created_at: 2025-03-17T02:21:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,115 +1,257 @@
 import { createColumn, createTaskCard } from './template.js';
-import { addChild } from '../utils/dom.js';
-import { openCardDeleteModal } from './modal.js';
-import { loadColumnData } from '../../store/column.js';
+import { pushChild, unshiftChild } from '../utils/dom.js';
+import { getISOStringNow, generateUUID } from '../utils/generalUtils.js';
+import { openDeleteCardModal } from './modal.js';
+import {
+  loadColumnsData,
+  updateCard,
+  removeCard,
+  getSortedCardsByDate,
+} from '../../store/column.js';
 
-// 전체 칼럼 생성: 초기 랜더링시
-function renderColumns(columnList) {
+// ──────────────────────────────
+//  1. 데이터 로딩 및 초기 렌더링
+// ──────────────────────────────
+
+// 전체 칼럼과 카드 렌더링
+async function initColumnAndCard() {
+  const columnsData = await loadColumnsData();
+
+  renderColumns(columnsData);
+  renderCardsForColumn(columnsData);
+
+  initKanbanEvents();
+  initSortButton();
+}
+
+// 전체 칼럼 생성
+function renderColumns(columnsData) {
   const columnContainer = document.querySelector('#columns-container');
 
-  const columnsHtml = columnList.reduce(
+  const columnsHtml = columnsData.reduce(
     (acc, { id, title, taskCount }) =>
       (acc += createColumn(id, title, taskCount)),
     ''
   );
 
-  addChild(columnContainer, columnsHtml);
+  pushChild(columnContainer, columnsHtml);
 }
 
 // 칼럼에 카드 생성
-function renderCardsForColumn(columnList) {
-  columnList.forEach(({ id, tasks }) => {
-    const columnCardList = document.querySelector(`#${id} .card-list`);
+function renderCardsForColumn(columnsData) {
+  columnsData.forEach(({ id, tasks }) => {
+    const columnCardList = document.querySelector(`#${id} .card-container`);
+
     const taskCardsHtml = tasks.reduce(
       (acc, { id, title, content, author }) =>
         (acc += createTaskCard(id, title, content, author)),
       ''
     );
 
-    addChild(columnCardList, taskCardsHtml);
+    pushChild(columnCardList, taskCardsHtml);
   });
 }
 
-// 칼럼과 카드 렌더링을 호출하는 메인 함수
-function renderColumnsAndCards(columnList) {
-  renderColumns(columnList); // 칼럼 생성
-  renderCardsForColumn(columnList); // 카드 추가
+// 칼럼에 카드 하나 생성
+function renderCard(columnId, cardData) {
+  const { id, title, content, author } = cardData;
+  const column = document.querySelector(`#${columnId} .card-container`);
+  const taskCardHtml = createTaskCard(id, title, content, author);
+
+  if (isSortCreated()) {
+    pushChild(column, taskCardHtml);
+  } else {
+    unshiftChild(column, taskCardHtml);
+  }
+}
+
+// 생성순 정렬 여부 반환
+function isSortCreated() {
+  const sortButtonLabel = document.querySelector('#sort-btn').dataset.type;
+  return sortButtonLabel === 'created';
 }
 
-// 각 칼럼의 카드 전부 제거
-function clearCards() {
-  document.querySelectorAll('.column').forEach(clearCardOfColumn);
+// ──────────────────────────────
+//  2. 카드 추가 및 삭제
+// ──────────────────────────────
 
-  function clearCardOfColumn(columnElement) {
-    const columnCardList = columnElement.querySelector('.card-list');
-    if (columnCardList) {
-      columnCardList.innerHTML = ''; // 각 칼럼의 카드 리스트만 비우기
-    }
-  }
+// 새 카드 생성
+function createNewCard(target) {
+  const column = getColumn(target);
+  if (!column) return;
+
+  const formCard = getCardForm(column);
+  if (!formCard) return;
+
+  const inputData = collectInputData(formCard);
+  if (!inputData) return;
+
+  closeCardForm(formCard);
+  saveAndRenderCard(column.id, inputData);
+  resetValues(formCard);
+}
+
+// 카드 데이터 저장 & UI 업데이트
+function saveAndRenderCard(columnId, inputData) {
+  updateCard(columnId, inputData);
+  renderCard(columnId, inputData);
+}
+
+// 입력 값 초기화
+function resetValues(formCard) {
+  formCard.querySelector('input').value = '';
+  formCard.querySelector('textarea').value = '';
 }
 
-// 카드 제거
+// 카드 제거 함수 반환 (고차함수)
 function makeCardRemover(cardId) {
-  return function () {
+  return () => {
     const targetCard = document.getElementById(cardId);
-    targetCard.remove();
+    const column = targetCard.closest('.column');
+    const columnId = column.id;
+
+    removeCard(columnId, cardId); // 데이터 제거
+    targetCard.remove(); // UI 제거
   };
 }
 
-//카드 제거 버튼 이벤트 추가 함수
-function initCardRemoveButton() {
-  const columnSection = document.getElementById('columns-container');
-  columnSection.addEventListener('click', openCardDeleteModal);
+// 각 칼럼의 카드 전부 제거
+function clearCards() {
+  document.querySelectorAll('.column').forEach(clearCardOfColumn);
+}
+
+// 특정 칼럼의 카드 제거
+function clearCardOfColumn(columnElement) {
+  const columnCardList = columnElement.querySelector('.card-container');
+  if (columnCardList) {
+    columnCardList.innerHTML = ''; // 카드 리스트 비우기
+  }
path: js/components/cardColumn.js
user: login: crongro
body: optional chaining으로.
created_at: 2025-03-17T02:21:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,115 +1,257 @@
 import { createColumn, createTaskCard } from './template.js';
-import { addChild } from '../utils/dom.js';
-import { openCardDeleteModal } from './modal.js';
-import { loadColumnData } from '../../store/column.js';
+import { pushChild, unshiftChild } from '../utils/dom.js';
+import { getISOStringNow, generateUUID } from '../utils/generalUtils.js';
+import { openDeleteCardModal } from './modal.js';
+import {
+  loadColumnsData,
+  updateCard,
+  removeCard,
+  getSortedCardsByDate,
+} from '../../store/column.js';
 
-// 전체 칼럼 생성: 초기 랜더링시
-function renderColumns(columnList) {
+// ──────────────────────────────
+//  1. 데이터 로딩 및 초기 렌더링
+// ──────────────────────────────
+
+// 전체 칼럼과 카드 렌더링
+async function initColumnAndCard() {
+  const columnsData = await loadColumnsData();
+
+  renderColumns(columnsData);
+  renderCardsForColumn(columnsData);
+
+  initKanbanEvents();
+  initSortButton();
+}
+
+// 전체 칼럼 생성
+function renderColumns(columnsData) {
   const columnContainer = document.querySelector('#columns-container');
 
-  const columnsHtml = columnList.reduce(
+  const columnsHtml = columnsData.reduce(
     (acc, { id, title, taskCount }) =>
       (acc += createColumn(id, title, taskCount)),
     ''
   );
 
-  addChild(columnContainer, columnsHtml);
+  pushChild(columnContainer, columnsHtml);
 }
 
 // 칼럼에 카드 생성
-function renderCardsForColumn(columnList) {
-  columnList.forEach(({ id, tasks }) => {
-    const columnCardList = document.querySelector(`#${id} .card-list`);
+function renderCardsForColumn(columnsData) {
+  columnsData.forEach(({ id, tasks }) => {
+    const columnCardList = document.querySelector(`#${id} .card-container`);
+
     const taskCardsHtml = tasks.reduce(
       (acc, { id, title, content, author }) =>
         (acc += createTaskCard(id, title, content, author)),
       ''
     );
 
-    addChild(columnCardList, taskCardsHtml);
+    pushChild(columnCardList, taskCardsHtml);
   });
 }
 
-// 칼럼과 카드 렌더링을 호출하는 메인 함수
-function renderColumnsAndCards(columnList) {
-  renderColumns(columnList); // 칼럼 생성
-  renderCardsForColumn(columnList); // 카드 추가
+// 칼럼에 카드 하나 생성
+function renderCard(columnId, cardData) {
+  const { id, title, content, author } = cardData;
+  const column = document.querySelector(`#${columnId} .card-container`);
+  const taskCardHtml = createTaskCard(id, title, content, author);
+
+  if (isSortCreated()) {
+    pushChild(column, taskCardHtml);
+  } else {
+    unshiftChild(column, taskCardHtml);
+  }
+}
+
+// 생성순 정렬 여부 반환
+function isSortCreated() {
+  const sortButtonLabel = document.querySelector('#sort-btn').dataset.type;
+  return sortButtonLabel === 'created';
 }
 
-// 각 칼럼의 카드 전부 제거
-function clearCards() {
-  document.querySelectorAll('.column').forEach(clearCardOfColumn);
+// ──────────────────────────────
+//  2. 카드 추가 및 삭제
+// ──────────────────────────────
 
-  function clearCardOfColumn(columnElement) {
-    const columnCardList = columnElement.querySelector('.card-list');
-    if (columnCardList) {
-      columnCardList.innerHTML = ''; // 각 칼럼의 카드 리스트만 비우기
-    }
-  }
+// 새 카드 생성
+function createNewCard(target) {
+  const column = getColumn(target);
+  if (!column) return;
+
+  const formCard = getCardForm(column);
+  if (!formCard) return;
+
+  const inputData = collectInputData(formCard);
+  if (!inputData) return;
+
+  closeCardForm(formCard);
+  saveAndRenderCard(column.id, inputData);
+  resetValues(formCard);
+}
+
+// 카드 데이터 저장 & UI 업데이트
+function saveAndRenderCard(columnId, inputData) {
+  updateCard(columnId, inputData);
+  renderCard(columnId, inputData);
+}
+
+// 입력 값 초기화
+function resetValues(formCard) {
+  formCard.querySelector('input').value = '';
+  formCard.querySelector('textarea').value = '';
 }
 
-// 카드 제거
+// 카드 제거 함수 반환 (고차함수)
 function makeCardRemover(cardId) {
-  return function () {
+  return () => {
     const targetCard = document.getElementById(cardId);
-    targetCard.remove();
+    const column = targetCard.closest('.column');
+    const columnId = column.id;
+
+    removeCard(columnId, cardId); // 데이터 제거
+    targetCard.remove(); // UI 제거
   };
 }
 
-//카드 제거 버튼 이벤트 추가 함수
-function initCardRemoveButton() {
-  const columnSection = document.getElementById('columns-container');
-  columnSection.addEventListener('click', openCardDeleteModal);
+// 각 칼럼의 카드 전부 제거
+function clearCards() {
+  document.querySelectorAll('.column').forEach(clearCardOfColumn);
+}
+
+// 특정 칼럼의 카드 제거
+function clearCardOfColumn(columnElement) {
+  const columnCardList = columnElement.querySelector('.card-container');
+  if (columnCardList) {
+    columnCardList.innerHTML = ''; // 카드 리스트 비우기
+  }
 }
 
-//정렬 버튼 클릭 이벤트
-function sortCards(columnList, { currentTarget }) {
+// ──────────────────────────────
+//  3. 카드 정렬 기능
+// ──────────────────────────────
+
+// 정렬 버튼 클릭 이벤트
+async function sortCards({ currentTarget }) {
   const sortButton = currentTarget;
-  const sortButtonName = sortButton.querySelector('.sort-btn-name');
+  const sortButtonLabel = sortButton.querySelector('.sort-btn-label');
   const currentSortType = sortButton.dataset.type;
 
+  const { newSortType, buttonText } = toggleSortType(currentSortType);
+
+  updateSortButtonUI(sortButton, sortButtonLabel, newSortType, buttonText);
path: js/components/cardColumn.js
user: login: crongro
body: 파라미터가 이렇게 많으면 객체형태로 하나의 파라미터로 전달하면 key 값도 있어서 좋아요.
created_at: 2025-03-17T02:21:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,14 @@
+function getISOStringNow() {
+  const date = new Date();
+  return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
+}
+
+function generateUUID() {
+  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
+    const random = (Math.random() * 16) | 0; // 0~15 사이의 랜덤 정수
path: js/utils/generalUtils.js
user: login: crongro
body: 16이런값을 파라미터로 받아도 될듯. 아니면 default parameter활용
created_at: 2025-03-17T02:24:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,73 +1,97 @@
 import { createHistoryItem } from './template.js';
-import { addChild } from '../utils/dom.js';
+import { pushChild } from '../utils/dom.js';
 import { openHistoryDeleteModal } from './modal.js';
 import { loadHistoryData } from '../../store/history.js';
 
+// ──────────────────────────────
+//  1. 캐싱된 DOM 요소(동적이 요소가 아닌 DOM 요소)
+// ──────────────────────────────
+const historyContainer = document.getElementById('history-container');
+const historyDefault = document.getElementById('history-default');
+const historyFooter = document.querySelector('#history footer');
+const historySection = document.getElementById('history');
+
+// ──────────────────────────────
+//  2. 기록 렌더링 및 삭제
+// ──────────────────────────────
+
+// 기록 렌더링
 function renderHistoryItems(historyList) {
   if (historyList.length === 0) {
-    toggleHistoryDefaultUi(true);
+    setHistoryDefaultUI(true);
     return;
   }
-  const historyContainer = document.querySelector('#history-container');
 
   const historyItemsHtml = historyList.reduce(
-    (acc, { username, profileImage, actionText, timestamp }) => {
+    (items, { username, profileImage, actionText, timestamp }) => {
       return (
-        acc + createHistoryItem(username, profileImage, actionText, timestamp)
+        items + createHistoryItem(username, profileImage, actionText, timestamp)
       );
     },
     ''
   );
 
-  addChild(historyContainer, historyItemsHtml);
-  toggleDeleteButton(true); // 기록삭제 버튼 보여주기
+  pushChild(historyContainer, historyItemsHtml);
+  setHistoryDeleteButton(true);
 }
 
-// Remove history record
+// 기록 삭제
 function removeHistoryRecords() {
-  document.getElementById('history-container').innerHTML = '';
-  toggleHistoryDefaultUi(true);
-  toggleDeleteButton(false); // 기록삭제 버튼 감추기
+  historyContainer.innerHTML = '';
+  setHistoryDefaultUI(true);
+  setHistoryDeleteButton(false);
 }
 
-//history 열기
+// ──────────────────────────────
+//  3. UI 토글 함수
+// ──────────────────────────────
+
+// 히스토리 열기/닫기
 function toggleHistory() {
-  const history = document.querySelector('#history');
-  const isVisable = window.getComputedStyle(history).display === 'flex';
+  setHistoryVisibility(historySection.style.display !== 'flex');
+}
 
-  history.style.display = isVisable ? 'none' : 'flex';
+// 히스토리 표시 여부 설정
+function setHistoryVisibility(isVisible) {
+  historySection.style.display = isVisible ? 'flex' : 'none';
 }
 
-// Toggle Default history
-function toggleHistoryDefaultUi(show) {
-  const historyDefault = document.getElementById('history-default');
-  historyDefault.style.display = show ? 'flex' : 'none';
+// 기본 UI 표시 여부
+function setHistoryDefaultUI(isVisible) {
+  historyDefault.style.display = isVisible ? 'flex' : 'none';
 }
 
-// Toggle History Footer
-function toggleDeleteButton(show) {
-  const footerElement = document.querySelector('#history footer');
-  footerElement.style.display = show ? 'flex' : 'none';
+// 기록 삭제 버튼 표시 여부
+function setHistoryDeleteButton(isVisible) {
+  historyFooter.style.display = isVisible ? 'flex' : 'none';
 }
 
-//History Event Listener
-function initHistoryButton() {
-  const historyButton = document.getElementById('history-open-btn');
-  historyButton.addEventListener('click', toggleHistory);
+// ──────────────────────────────
+//  4. 이벤트 핸들링
+// ──────────────────────────────
 
-  const historyCloseButton = document.getElementById('history-close-btn');
-  historyCloseButton.addEventListener('click', toggleHistory);
+// 이벤트 리스너 등록
+function initHistoryButtons() {
+  const buttonMap = new Map([
path: js/components/history.js
user: login: crongro
body: Map과 Object의 차이점은? 
created_at: 2025-03-17T02:24:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,73 +1,97 @@
 import { createHistoryItem } from './template.js';
-import { addChild } from '../utils/dom.js';
+import { pushChild } from '../utils/dom.js';
 import { openHistoryDeleteModal } from './modal.js';
 import { loadHistoryData } from '../../store/history.js';
 
+// ──────────────────────────────
+//  1. 캐싱된 DOM 요소(동적이 요소가 아닌 DOM 요소)
+// ──────────────────────────────
+const historyContainer = document.getElementById('history-container');
+const historyDefault = document.getElementById('history-default');
+const historyFooter = document.querySelector('#history footer');
+const historySection = document.getElementById('history');
path: js/components/history.js
user: login: crongro
body: historySection에서 history를 찾았으니, 
historyFooter = history.querySelector("footer") 
라고 하면 될 듯 
created_at: 2025-03-17T02:26:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,14 @@
+function getISOStringNow() {
+  const date = new Date();
+  return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
+}
+
+function generateUUID() {
+  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
+    const random = (Math.random() * 16) | 0; // 0~15 사이의 랜덤 정수
path: js/utils/generalUtils.js
user: login: wan0514
body: > 16이런값을 파라미터로 받아도 될듯. 아니면 default parameter활용

파라미터로 받음으로서 얻는 장점이 뭔가요?
created_at: 2025-03-18T02:18:45Z
author_association: NONE
diff_hunk: @@ -1,115 +1,257 @@
 import { createColumn, createTaskCard } from './template.js';
-import { addChild } from '../utils/dom.js';
-import { openCardDeleteModal } from './modal.js';
-import { loadColumnData } from '../../store/column.js';
+import { pushChild, unshiftChild } from '../utils/dom.js';
+import { getISOStringNow, generateUUID } from '../utils/generalUtils.js';
+import { openDeleteCardModal } from './modal.js';
+import {
+  loadColumnsData,
+  updateCard,
+  removeCard,
+  getSortedCardsByDate,
+} from '../../store/column.js';
 
-// 전체 칼럼 생성: 초기 랜더링시
-function renderColumns(columnList) {
+// ──────────────────────────────
+//  1. 데이터 로딩 및 초기 렌더링
+// ──────────────────────────────
+
+// 전체 칼럼과 카드 렌더링
+async function initColumnAndCard() {
+  const columnsData = await loadColumnsData();
+
+  renderColumns(columnsData);
+  renderCardsForColumn(columnsData);
+
+  initKanbanEvents();
+  initSortButton();
+}
+
+// 전체 칼럼 생성
+function renderColumns(columnsData) {
   const columnContainer = document.querySelector('#columns-container');
 
-  const columnsHtml = columnList.reduce(
+  const columnsHtml = columnsData.reduce(
     (acc, { id, title, taskCount }) =>
       (acc += createColumn(id, title, taskCount)),
     ''
   );
 
-  addChild(columnContainer, columnsHtml);
+  pushChild(columnContainer, columnsHtml);
 }
 
 // 칼럼에 카드 생성
-function renderCardsForColumn(columnList) {
-  columnList.forEach(({ id, tasks }) => {
-    const columnCardList = document.querySelector(`#${id} .card-list`);
+function renderCardsForColumn(columnsData) {
+  columnsData.forEach(({ id, tasks }) => {
+    const columnCardList = document.querySelector(`#${id} .card-container`);
+
     const taskCardsHtml = tasks.reduce(
       (acc, { id, title, content, author }) =>
         (acc += createTaskCard(id, title, content, author)),
       ''
     );
 
-    addChild(columnCardList, taskCardsHtml);
+    pushChild(columnCardList, taskCardsHtml);
   });
 }
 
-// 칼럼과 카드 렌더링을 호출하는 메인 함수
-function renderColumnsAndCards(columnList) {
-  renderColumns(columnList); // 칼럼 생성
-  renderCardsForColumn(columnList); // 카드 추가
+// 칼럼에 카드 하나 생성
+function renderCard(columnId, cardData) {
+  const { id, title, content, author } = cardData;
+  const column = document.querySelector(`#${columnId} .card-container`);
+  const taskCardHtml = createTaskCard(id, title, content, author);
+
+  if (isSortCreated()) {
+    pushChild(column, taskCardHtml);
+  } else {
+    unshiftChild(column, taskCardHtml);
+  }
+}
+
+// 생성순 정렬 여부 반환
+function isSortCreated() {
+  const sortButtonLabel = document.querySelector('#sort-btn').dataset.type;
+  return sortButtonLabel === 'created';
 }
 
-// 각 칼럼의 카드 전부 제거
-function clearCards() {
-  document.querySelectorAll('.column').forEach(clearCardOfColumn);
+// ──────────────────────────────
+//  2. 카드 추가 및 삭제
+// ──────────────────────────────
 
-  function clearCardOfColumn(columnElement) {
-    const columnCardList = columnElement.querySelector('.card-list');
-    if (columnCardList) {
-      columnCardList.innerHTML = ''; // 각 칼럼의 카드 리스트만 비우기
-    }
-  }
+// 새 카드 생성
+function createNewCard(target) {
+  const column = getColumn(target);
+  if (!column) return;
+
+  const formCard = getCardForm(column);
+  if (!formCard) return;
+
+  const inputData = collectInputData(formCard);
+  if (!inputData) return;
+
+  closeCardForm(formCard);
+  saveAndRenderCard(column.id, inputData);
+  resetValues(formCard);
+}
+
+// 카드 데이터 저장 & UI 업데이트
+function saveAndRenderCard(columnId, inputData) {
+  updateCard(columnId, inputData);
+  renderCard(columnId, inputData);
+}
+
+// 입력 값 초기화
+function resetValues(formCard) {
+  formCard.querySelector('input').value = '';
+  formCard.querySelector('textarea').value = '';
 }
 
-// 카드 제거
+// 카드 제거 함수 반환 (고차함수)
 function makeCardRemover(cardId) {
-  return function () {
+  return () => {
     const targetCard = document.getElementById(cardId);
-    targetCard.remove();
+    const column = targetCard.closest('.column');
+    const columnId = column.id;
+
+    removeCard(columnId, cardId); // 데이터 제거
+    targetCard.remove(); // UI 제거
   };
 }
 
-//카드 제거 버튼 이벤트 추가 함수
-function initCardRemoveButton() {
-  const columnSection = document.getElementById('columns-container');
-  columnSection.addEventListener('click', openCardDeleteModal);
+// 각 칼럼의 카드 전부 제거
+function clearCards() {
+  document.querySelectorAll('.column').forEach(clearCardOfColumn);
+}
+
+// 특정 칼럼의 카드 제거
+function clearCardOfColumn(columnElement) {
+  const columnCardList = columnElement.querySelector('.card-container');
+  if (columnCardList) {
+    columnCardList.innerHTML = ''; // 카드 리스트 비우기
+  }
path: js/components/cardColumn.js
user: login: wan0514
body: > optional chaining으로.

columnCardList가 객체의 속성이 아니라 node 요소라서 옵셔녈 체이닝이 문법적 오류가 발생합니다. 
대신 && 연산자 사용하여 짧게 줄였습니다!
created_at: 2025-03-18T02:51:06Z
author_association: NONE
diff_hunk: @@ -1,73 +1,97 @@
 import { createHistoryItem } from './template.js';
-import { addChild } from '../utils/dom.js';
+import { pushChild } from '../utils/dom.js';
 import { openHistoryDeleteModal } from './modal.js';
 import { loadHistoryData } from '../../store/history.js';
 
+// ──────────────────────────────
+//  1. 캐싱된 DOM 요소(동적이 요소가 아닌 DOM 요소)
+// ──────────────────────────────
+const historyContainer = document.getElementById('history-container');
+const historyDefault = document.getElementById('history-default');
+const historyFooter = document.querySelector('#history footer');
+const historySection = document.getElementById('history');
+
+// ──────────────────────────────
+//  2. 기록 렌더링 및 삭제
+// ──────────────────────────────
+
+// 기록 렌더링
 function renderHistoryItems(historyList) {
   if (historyList.length === 0) {
-    toggleHistoryDefaultUi(true);
+    setHistoryDefaultUI(true);
     return;
   }
-  const historyContainer = document.querySelector('#history-container');
 
   const historyItemsHtml = historyList.reduce(
-    (acc, { username, profileImage, actionText, timestamp }) => {
+    (items, { username, profileImage, actionText, timestamp }) => {
       return (
-        acc + createHistoryItem(username, profileImage, actionText, timestamp)
+        items + createHistoryItem(username, profileImage, actionText, timestamp)
       );
     },
     ''
   );
 
-  addChild(historyContainer, historyItemsHtml);
-  toggleDeleteButton(true); // 기록삭제 버튼 보여주기
+  pushChild(historyContainer, historyItemsHtml);
+  setHistoryDeleteButton(true);
 }
 
-// Remove history record
+// 기록 삭제
 function removeHistoryRecords() {
-  document.getElementById('history-container').innerHTML = '';
-  toggleHistoryDefaultUi(true);
-  toggleDeleteButton(false); // 기록삭제 버튼 감추기
+  historyContainer.innerHTML = '';
+  setHistoryDefaultUI(true);
+  setHistoryDeleteButton(false);
 }
 
-//history 열기
+// ──────────────────────────────
+//  3. UI 토글 함수
+// ──────────────────────────────
+
+// 히스토리 열기/닫기
 function toggleHistory() {
-  const history = document.querySelector('#history');
-  const isVisable = window.getComputedStyle(history).display === 'flex';
+  setHistoryVisibility(historySection.style.display !== 'flex');
+}
 
-  history.style.display = isVisable ? 'none' : 'flex';
+// 히스토리 표시 여부 설정
+function setHistoryVisibility(isVisible) {
+  historySection.style.display = isVisible ? 'flex' : 'none';
 }
 
-// Toggle Default history
-function toggleHistoryDefaultUi(show) {
-  const historyDefault = document.getElementById('history-default');
-  historyDefault.style.display = show ? 'flex' : 'none';
+// 기본 UI 표시 여부
+function setHistoryDefaultUI(isVisible) {
+  historyDefault.style.display = isVisible ? 'flex' : 'none';
 }
 
-// Toggle History Footer
-function toggleDeleteButton(show) {
-  const footerElement = document.querySelector('#history footer');
-  footerElement.style.display = show ? 'flex' : 'none';
+// 기록 삭제 버튼 표시 여부
+function setHistoryDeleteButton(isVisible) {
+  historyFooter.style.display = isVisible ? 'flex' : 'none';
 }
 
-//History Event Listener
-function initHistoryButton() {
-  const historyButton = document.getElementById('history-open-btn');
-  historyButton.addEventListener('click', toggleHistory);
+// ──────────────────────────────
+//  4. 이벤트 핸들링
+// ──────────────────────────────
 
-  const historyCloseButton = document.getElementById('history-close-btn');
-  historyCloseButton.addEventListener('click', toggleHistory);
+// 이벤트 리스너 등록
+function initHistoryButtons() {
+  const buttonMap = new Map([
path: js/components/history.js
user: login: wan0514
body: > Map과 Object의 차이점은?

Map
- 삽입 순서 보장
- 검색/삭제/반복 성능이 빠름
- 다양한 데이터타입을 key로 설정이 가능
- 대량읜 데이터에 적합

Object
- 삽입 순서 보장 x
- Map보다 검색/삭제/반복이 느림
- 문자열과 symbol만 key값으로 설정 가능
- 간단한 데이터에 적합

Map을 선택한 이유는 이벤트 위임에서 분기처리할 게 앞으로 많아지고, 이를 반복하며 분기를 찾기 때문에
반복 성능을 높이기 위해 Map을 사용했습니다.
created_at: 2025-03-18T03:03:26Z
author_association: NONE

PR comments:
user: login: crongro
created_at: 2025-03-14T03:38:53Z
body: > * 각 카드가 고유한 식별자를 갖도록 UUID를 도입

UUID는 무엇이고, 어떤 알고리즘으로 만들어질까요? 
author_association: CONTRIBUTOR
user: login: crongro
created_at: 2025-03-14T03:43:28Z
body: > * 질 경우 전체 재렌더링이 **비효율적**이라 판단하여 **UI 직접 조작 방식(옵션 2번)을 선택**.

과정이 좋네요. 이유와 함께 잘 선택할 필요가 있죠.
author_association: CONTRIBUTOR
user: login: crongro
created_at: 2025-03-14T03:49:07Z
body: > * 주석을 이용해 **함수들을 카테고리별로 묶고, 가독성을 높임**.

실제로 비슷한 묶음을 모아두는것도 좋긴해요.
정말 그룹을 지으려면 객체리터럴로 묶는것도 가능.  (실제로 이걸 namespace패턴이라고도 했었음. 과거에)
author_association: CONTRIBUTOR