[ 과제4 ] 자바스크립트를 이용해서 Spread Sheet 앱 만들기

김민지·2024년 9월 21일
0

과제 내용

  • 현재 focus 된 셀의 위쪽 헤더 왼쪽 헤더가 함께 하늘색으로 하이라이트 되도록 해줌
  • 작성된 모든 데이터들을 Export SpreadSheet 버튼을 눌러서 Excel 파일로 생성
  • 생성된 Excel 파일을 구글 Spreadsheet 에서 import하면 같은 데이터가 나오도록 함

진행

  • HTML로 기본 틀 생성 + CSS로 스타일링 → 이 때, Export 버튼, cell: 까지만 생성하고 스프레드시트는 JS로 생성해야 함

JS로 스프레드시트 생성

  1. initSpreadSheet() 함수로 기본데이터 생성하기
// 위의 console 출력을 위한 코드:
const ROWS = 10;
const COLS = 10;
const spreadsheet = [];

initSpreadsheet()

function initSpreadsheet() {
  for (let i = 0; i < ROWS; i++) { 
    let spreadsheetRow = []
    for (let j = 0; j < COLS; j++) {
      spreadsheetRow.push(i + "-" + j)
    }
    spreadsheet.push(spreadsheetRow)
  }
  console.log(spreadsheet);
}
  1. 클래스 Cell 만들기: 그러나, 위의 데이터는 문자열 데이터이다. 우리는 각 cell의 정보와 상태를 효과적으로 관리하기 위해 문자열이 아닌 객체 데이터를 생성해야 한다.
// 객체를 생성하기 위한 설계도인 클래스를 만든다.  
class Cell { 
  constructor(isHeader, disabled, data, row, column, active = false){
    this.isHeader = isHeader;
    this.disabled = disabled;
    this.data = data;
    this.row = row;
    this.column = column;
    this.active = active;
  }
}
  1. 클래스 개념 정리
  • 자바스크립트의 클래스? Cell
  • 자바스크립트의 객체? const cell1 = new Cell(true, false, "Header Data", 1, 1);
    즉, 자바스크립트에서는 클래스에서 객체를 설계하고, new 키워드와 클래스를 사용해서 생성되는 것이 객체이다.
// 클래스 이해를 위한 예제: 
class Cell {
  constructor(isHeader, disabled, data, row, column, active = false) {
    this.isHeader = isHeader;
    this.disabled = disabled;
    this.data = data;
    this.row = row;
    this.column = column;
    this.active = active;  
    // 다르게도 할당 가능
    this.twiceRow = row * 2;  // row 값의 두 배를 this.twiceRow에 할당
    this.constantValue = 100;  // 상수값을 this.constantValue에 할당
  }

  toggleActive() {
    this.active = !this.active;  // active 값을 반전시킴
  }
}

// toggleActive와 같은 메소드를 클래스 내에 정의했을 때의 동작을 이해하기 위한 예제: 

const cell1 = new Cell(true, false, "Header Data", 1, 1, true);
console.log(cell1.active);  // true

cell1.toggleActive();  
console.log(cell1.active);  // false (반전됨)
  1. 텍스트를 객체로 변경해서 출력하고자 하는 결과 화면:
  • 이를 위한 코드: 수정된 initSpreadsheet 함수
function initSpreadsheet() {
  for (let i = 0; i < ROWS; i++) { 
    let spreadsheetRow = []
    for (let j = 0; j < COLS; j++) {
      // spreadsheetRow.push(i + "-" + j)
      const cell = new Cell(false, false, i + "-" + j, i, j, i, j, false);
      spreadsheetRow.push(cell);
    }
    spreadsheet.push(spreadsheetRow)
  }
  // 실제로 그리는 코드 (5)번 추가
  drawSheet();
  console.log(spreadsheet); // console 창에 데이터 배열만 출력
}
  1. 화면에 보일 스프레드시트 부분 JS로 생성하기: createCellEl 함수로 input 태그를 가진 cell들을 생성해야 함
  • 이 때, cell의 id가 cell_03이면 첫번째 줄 세번째 열을 뜻함. 즉, 행-열 순으로 표기되어있음!
function createCellEl(cell) { // cell 하나를 생성하는 코드
    const cellEl = document.createElement('input');
    cellEl.className = 'cell';
    cellEl.id = 'cell_' + cell.row + cell.column; // 열 + 행
    cellEl.value = cell.data;
    cellEl.disabled = cell.disabled;
    return cellEl;
}

// spreadsheet는 앞서 만든 객체를 담은 배열 데이터

function drawSheet() { // sheet에 cell을 붙여서 sheet를 그리는 코드
    for (let i = 0; i < spreadsheet.length; i++) {
        for (let j = 0; j < spreadsheet[i].length; j++) {
            const cell = spreadsheet[i][j];
            spreadSheetContainer.append(createCellEl(cell));
        }
    }
}

  1. 생성된 input 요소 cell들을 10개씩 묶어서 하나의 row div 태그로 감싸주기 위해 drawSheet 함수 수정
    지금은 그냥 단순히 cell 100개가 한줄로 생성된 상황 (화면의 크기 때문에 여러줄에 거쳐서 보이지만 사실상 줄은 없음)
function drawSheet() {
    for (let i = 0; i < spreadsheet.length; i++) {
        const rowContainerEl = document.createElement("div"); // 10개씩 묶어서 감싸줄 div 생성
        rowContainerEl.className = "cell-row"; // class 이름 붙임

        for (let j = 0; j < spreadsheet[i].length; j++) {
            const cell = spreadsheet[i][j];
            rowContainerEl.append(createCellEl(cell)); // 곧바로 전체 spreadSheetContainer div에 추가하는 것이 아닌, 10개씩 묶어준 rowContainerEl에 추가해줌
        }
        spreadSheetContainer.append(rowContainerEl); // 이제 전체 spreadSheetContainer div에 추가해줌
    }
}
  1. css 설정해주기
.cell-row {
    display: flex;
}

.cell {
    width: 80px;
    border: 1px solid lightgray;
    height: 40px;
    outline: none;
    box-sizing: border-box;
}
  1. 표의 제일 왼쪽 열에 "", 1, 2, 3, 4, ... 등을 넣어서 엑셀과 비슷하게 만들어주기: initSpreadsheet 함수 수정
  • disabled, isHeader 속성도 설정해주기
const alphabets = [
    "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
]

function initSpreadsheet() {
    for (let i = 0; i < ROWS; i++) {
        let spreadsheetRow = [];
        for (let j = 0; j < COLS; j++) {
            let cellData = '';
            let isHeader = false;
            let disabled = false;

            // 모든 row 첫 번째 컬럼에 숫자 넣기
          	// 1, 2, 3, 4, ...
            if (j === 0) {
                cellData = i;
                isHeader = true;
                disabled = true;
            }
			// A, B, C, D, E, ... 
            if (i === 0) {
                cellData = alphabets[j - 1];
                isHeader = true;
                disabled = true;
            }

            // cellData가 Undefined이면 첫 번째 row의 컬럼은 "";
            if (!cellData) {
                cellData = "";
            }

            const rowName = i;
            const columnName = alphabets[j - 1];

            const cell = new Cell(isHeader, disabled, cellData, i, j, rowName, columnName, false);
            spreadsheetRow.push(cell);
        }
        spreadsheet.push(spreadsheetRow);
    }
    drawSheet();
    // console.log(spreadsheet);
}
  1. header들만 따로 스타일링 해주기 위해 class="cell header"로 header를 추가해서 변경해주기 위해 createCellEl 함수 수정
function createCellEl(cell) {
    const cellEl = document.createElement('input');
    cellEl.className = 'cell';
    cellEl.id = 'cell_' + cell.row + cell.column;
    cellEl.value = cell.data;
    cellEl.disabled = cell.disabled;

  	// 앞서 설정해준 isHeader 옵션을 사용해서 필터링 해가면서 클래스에 "header" 추가하기
    if (cell.isHeader) { 
        cellEl.classList.add("header");
    }
    return cellEl;
}
  1. css에서 스타일링 수정
.cell.header {
    text-align: center;
    background: #ddd;
}
  1. Cell 클래스에 rowName, colName 속성을 추가하기
class Cell { // 클래스는 객체를 생성하기 위한 설계도이다. 
    constructor(isHeader, disabled, data, row, column, rowName, columnName, active = false) { // 매개변수들
    // constructor 함수: 클래스의 인스턴스가 생성될 때 호출되는 함수로, 객체의 초기 상태(프로퍼티)를 설정하는 역할
        this.isHeader = isHeader;
        this.disabled = disabled;
        this.data = data;
        this.row = row;
        this.column = column;
        this.rowName = rowName;
        this.columnName = columnName;
        this.active = active;
    }
}
  1. cell을 클릭했을 때 위치 값을 반환하는 함수를 createCellEl에 추가
function createCellEl(cell) {
    const cellEl = document.createElement('input');
    cellEl.className = 'cell';
    cellEl.id = 'cell_' + cell.row + cell.column;
    cellEl.value = cell.data;
    cellEl.disabled = cell.disabled;

    if (cell.isHeader) {
        cellEl.classList.add("header");
    }

    cellEl.onclick = () => handleCellClick(cell);
    cellEl.onchange = (e) => handleOnChange(e.target.value, cell);

    return cellEl;
}
  • handleCellClick: cell이 클릭되었을 때 해당 cell이 속해있는 header 2개의 데이터를 가져오도록 해야 함 (highlight css 구현하기 위해서)
function handleCellClick(cell) {
    const columnHeader = spreadsheet[0][cell.column];
    const rowHeader = spreadsheet[cell.row][0];
  
    const columnHeaderEl = getElFromRowCol(columnHeader.row, columnHeader.column);
    const rowHeaderEl = getElFromRowCol(rowHeader.row, rowHeader.column);
    columnHeaderEl.classList.add('active');
    rowHeaderEl.classList.add('active');
    // console.log('clicked cell', columnHeaderEl, rowHeaderEl);
    document.querySelector("#cell-status").innerHTML = cell.columnName + cell.rowName;
}
  • getElFromRowCol
function getElFromRowCol(row, col) {
    return document.querySelector("#cell_" + row + col);
}
  • handleOnChange
function handleOnChange(data, cell) {
    cell.data = data;
}
  • css
.cell.header.active {
    background: lightblue;
    color: white;
}
  1. 이전에 highlight 된 부분을 지워주기 위해서 .active 클래스명을 지워주기
function handleCellClick(cell) {
    clearHeaderActiveStates();
    const columnHeader = spreadsheet[0][cell.column];
    const rowHeader = spreadsheet[cell.row][0];
    const columnHeaderEl = getElFromRowCol(columnHeader.row, columnHeader.column);
    const rowHeaderEl = getElFromRowCol(rowHeader.row, rowHeader.column);
    columnHeaderEl.classList.add('active');
    rowHeaderEl.classList.add('active');
    // console.log('clicked cell', columnHeaderEl, rowHeaderEl);
    document.querySelector("#cell-status").innerHTML = cell.columnName + cell.rowName;
}

function clearHeaderActiveStates() {
    const headers = document.querySelectorAll('.header');

    headers.forEach((header) => {
        header.classList.remove('active');
    })
}

엑셀파일 다운받기

  1. 엑셀파일로 변환하기 위해서는 데이터를 아래와 같이 ,로 구분된 형태로 변환해야 함

exportBtn.onclick = function (e) {
    let csv = "";
    for (let i = 0; i < spreadsheet.length; i++) {
        if (i === 0) continue; // 없으면 첫줄이 비어서 나옴
        csv +=
            spreadsheet[i]
                .filter(item => !item.isHeader) // Header 빼기
                .map(item => item.data) // 많은 속성 중 data만
                .join(',') + "\r\n"; // ,를 사이사이 끼워서 조인
    }

    const csvObj = new Blob([csv]);
    console.log('csvObj', csvObj);

    const csvUrl = URL.createObjectURL(csvObj);
    console.log('csvUrl', csvUrl);

    const a = document.createElement("a");
    a.href = csvUrl;
    a.download = 'spreadsheet name.csv';
    a.click();
}

querySelector vs getElementById

  1. querySelector: 제공한 선택자 또는 선택자 뭉치와 일치하는 문서 내 첫 번째 Element를 반환하고, 일치하는 요소가 없으면 null을 반환
  2. getElementById: 주어진 문자열과 일치하는 id 속성을 가진 요소를 찾고, 이를 나타내는 Element 객체를 반환
  • ID는 문서 내에서 유일해야 하기 때문에 특정 요소를 빠르게 찾을 때 유용함
// querySelector
document.querySelector(".myclass"); // class
document.querySelector("#export-btn"); // id

// getElementById
document.getElementById("myid"); // id만 가능

0개의 댓글