Never give up

React, Node - Rest API with sqlite3 (beginner) - 2 본문

WEB

React, Node - Rest API with sqlite3 (beginner) - 2

대기만성 개발자 2022. 1. 8. 01:05
반응형

이전 포스트에 이어서 react부분을 정리해봤습니다

(링크 : https://devmemory.tistory.com/78)

 

다만 class형만 다루고 함수형은 나중에 숙련도가 조금 더 올라가면

 

함수형으로 간단한 예제를 한번 만들어볼까 합니다

 

api

import { response } from 'util/util'
const baseURL = "api"

// 성공: code: 1, data : object
// 실패: code: -1, data : 실패 message
class API {
    getTasks = async (pageNo, pageSize) => {
        let url = `${baseURL}/task`

        if(pageNo){
            url += `?pageNo=${pageNo}`
        }

        if(pageSize){
            url += `&pageSize=${pageSize}`
        }

        console.log(url)

        const res = await response(url)

        return {
            data: res.data,
            code: res.code
        }
    }

    getSingleTask = async (id) => {
        const res = await response(`${baseURL}/task/${id}`)

        return {
            data: res.data,
            code: res.code
        }
    }

    // task
    addTask = async (task) => {
        const res = await response(`${baseURL}/task/add`, 'POST', task)

        return {
            data: res.data,
            code: res.code
        }
    }

    deleteTask = async (id) => {
        const res = await response(`${baseURL}/task/delete`, 'POST', { id })

        return {
            data: res.data,
            code: res.code
        }
    }

    toggleReminder = async (id) => {
        const res = await response(`${baseURL}/task/toggle`, 'POST', { id })

        return {
            data: res.data,
            code: res.code
        }
    }
}

export { API }

util

import axios from "axios"

// fetch
const tryFetch = async (url,option) => {
    let data
    let code

    try {
        const res = await fetch(url,option)

        const jsonData = await res.json()

        data = jsonData.data
        code = jsonData.code

        if(res.status !== 200){
            throw new Error(`Failed to fetch. status : ${res.status}`)
        }
    } catch(e){
        data = e
        code = -1
    }

    return {
        data,
        code
    }
}

// axios
const response = async (url,method,data) => {
    let result
    let code

    try {
        const res = await axios({url, method, data})

        result = res.data.data
        code = res.data.code

        if(res.status !== 200){
            throw new Error(`Failed to fetch. status : ${res.status}`)
        }
    } catch(e){
        result = e
        code = -1
    }

    return {
        data: result,
        code
    }
}

export { tryFetch, response }

axios, fetch테스트용도로 일단 만들어놨는데 전반적으로 axios가 조금 더 간단한거 같은데

 

패키지 설치 및 업데이트를 생각하면 fetch를 사용하는것도 괜찮을것 같다는 생각이 들었습니다

 

그리고 js특성상 status code가 다른게 넘어와도

 

error로 인식을 안되어 별도로 처리해줘야되는 부분이 조금 아쉬웠던것 같습니다

 

app

import { Component } from "react"
import Header from 'component/todo/class/header'
import Tasks from 'component/todo/class/tasks'
import AddTask from 'component/todo/class/add_task'
import { API } from 'api/common'
import { Center } from "style/styled"
import 'style/task_style.css'
import { Spinner } from "react-bootstrap"
import PaginationButton from "component/todo/class/pagination_button"

class ClassApp extends Component {
    constructor() {
        super()

        this.state = {
            showAddTask: false,
            tasks: [],
            isLoaded: false
        }

        this.page = {}

        this.pageSize = 3

        this.api = new API()

        this.addTask = this.addTask.bind(this)
        this.deleteTask = this.deleteTask.bind(this)
        this.toggleReminder = this.toggleReminder.bind(this)
        this.pagination = this.pagination.bind(this)
    }

    async initTask() {
        // this.api.getTasksAjax()

        const result = await this.api.getTasks(1, this.pageSize)
        if (result.code === 1) {
            this.page = {
                totalCount: result.data.totalCount,
                currentPage: result.data.currentPage,
                lastPage: Math.ceil(result.data.totalCount / this.pageSize)
            }

            this.setState({ tasks: result.data.list, isLoaded: true })
        } else {
            alert(result.data)
        }
    }

    async pagination(index) {
        if (index <= this.page.lastPage) {
            const result = await this.api.getTasks(index, this.pageSize)

            if (result.code === 1) {
                this.page.currentPage = result.data.currentPage

                this.setState({ tasks: result.data.list })
            } else {
                alert(result.data)
            }
        } else {
            alert('Last Page!')
        }
    }

    componentDidMount() {
        this.initTask()
    }

    async addTask(task) {
        const result = await this.api.addTask(task)
        if (result.code === 1) {
            if (this.state.tasks) {
                this.setState({ tasks: [result.data, ...this.state.tasks] })
            } else {
                this.setState({ tasks: [result.data] })
            }
        } else {
            alert(result.data)
        }
    }

    async deleteTask(id) {
        const result = await this.api.deleteTask(id)
        if (result.code === 1) {
            const task = this.state.tasks.find((element) => element.id === result.data)
            task.hide = true

            this.setState({ tasks: this.state.tasks.filter((element) => !element.hide) })
        } else {
            alert(result.data)
        }
    }

    async toggleReminder(id) {
        const result = await this.api.toggleReminder(id)
        if (result.code === 1) {
            const task = this.state.tasks.find((element) => element.id === result.data)
            task.reminder = !task.reminder

            this.setState({ tasks: this.state.tasks })
        } else {
            alert(result.data)
        }
    }

    render() {
        if (!this.state.isLoaded) {
            return (
                <Center height='100vh'>
                    <Spinner animation='grow' />
                </Center>
            )
        }

        return (
            <div className="div-container">
                <Header
                    onAdd={() => this.setState({ showAddTask: !this.state.showAddTask })}
                    showAdd={this.state.showAddTask} />

                {this.state.showAddTask && <AddTask onAdd={this.addTask} />}

                {(this.state.tasks?.length ?? -1) > 0 ?
                    <Tasks tasks={this.state.tasks} onDelete={this.deleteTask} onToggle={this.toggleReminder} /> : 'No tasks to show'}

                <PaginationButton page={this.page} pagination={this.pagination} />
            </div>
        )
    }
}

export default ClassApp;

이전과 다르게 init부분에 처리가 조금 더 많아졌는데,

 

pagination을 구현하기 위해서 pageModel을 추가했습니다

 

제가 사용할 데이터는 전체 개수, 현재 페이지, 그리고 마지막 페이지(계산식)

 

마지막 페이지는 (전체 개수 / pageSize)를 해서 ceil값을 가져옵니다

 

예를들어 45 / 10 이면 4.5인데 여기서 4로 처리를 해버리면

 

4 페이지(40개)만 보여주고 나머지 5개를 못보여주기 때문에 ceil을 사용했습니다

 

그리고 pagination 함수에서 page button을 누를때마다

 

페이지에 따른 api 콜 및 화면 갱신(setState)를 해주고 있습니다

 

styled

import styled from "styled-components";

export const PageButton = styled.span`
    margin: 4px;
    padding: 6px;
    background: ${(props) => props.background};
    color: white;
    cursor: pointer;
    text-align: center;
    font-size: 14px;

    &:hover {
        background: #b4b4b4
    }
`;

pagination button

import { Component } from 'react'
import { PageButton } from 'style/styled'
import { AiFillCaretLeft, AiFillCaretRight } from "react-icons/ai"

class PaginationButton extends Component {
    constructor(props) {
        super(props)

        this.pageRange = 1

        this.lastPageRange = Math.ceil(props.page.lastPage / 10)

        this.pageList = this.pageList.bind(this)
        this.changeRange = this.changeRange.bind(this)
    }

    pageList() {
        const start = (this.pageRange - 1) * 10 + 1
        const end = 10 * this.pageRange > this.props.page.lastPage ? this.props.page.lastPage : 10 * this.pageRange

        return Array(end - start + 1).fill().map((_, index) => start + index)
    }

    changeRange(isAdd) {
        this.pageRange = isAdd ? this.pageRange + 1 : this.pageRange - 1

        this.props.pagination(isAdd ? (this.pageRange - 1) * 10 + 1 : this.pageRange * 10)
    }

    render() {
        const { page, pagination } = this.props
        return (
            <>
                {this.pageRange > 1 ? <PageButton key='previous' background='grey' onClick={() => this.changeRange(false)}>
                    <AiFillCaretLeft />
                </PageButton> : <></>}

                {this.pageList().map((e) => (
                    <PageButton key={e} background={page.currentPage === e ? '#bebebe' : 'grey'} onClick={() => pagination(e)}>
                        {e}
                    </PageButton>
                ))}

                {this.lastPageRange > this.pageRange ? <PageButton key='next' background='grey' onClick={() => this.changeRange(true)}>
                    <AiFillCaretRight />
                </PageButton> : <></>}
            </>
        )
    }
}

export default PaginationButton

 

이부분은 필자가 고민을 조금한 포인트가 있었는데

 

페이지 리스트 생성부분 그리고 상태 변경부분이었는데

 

리스트 생성부분은

 

1~10, 11~20, 21~30 => (n-1) * 10 + 1 ~ 10 * n으로 start, end포인트를 만들고

 

Array(end - start + 1).fill()을 해주면 개수가 10인 빈값 배열이 생성되고

 

map을 이용해서 start + index를 해줘서 1~10, 11~20등을 만들어줍니다

 

페이지 범위 변경부분은 다행이도(?) app부분에서 setState가 일어나서

 

해당 컴포넌트에서는 추가로 처리를 안해줘도 잘 작동이 되었습니다

 

task

import { Component } from 'react'
import { FaTimes } from 'react-icons/fa'

class Task extends Component {
    render() {
        const { task, onDelete, onToggle } = this.props
        return (
            <div className={`task ${task.reminder ? 'reminder' : ''}`} onDoubleClick={() => onToggle(task.id)}>
                <h5>
                    {task.title} <FaTimes style={{ color: 'red', cursor: 'pointer' }} onClick={() => onDelete(task.id)} />
                </h5>
                <p>{task.day}</p>
            </div>
        )
    }
}

export default Task

tasks

import { Component } from 'react'
import Task from './task'

class Tasks extends Component {
    render() {
        const { tasks, onDelete, onToggle } = this.props
        return (
            <div>
                {tasks.map((task) => (
                    <Task key={task.id} task={task} onDelete={onDelete} onToggle={onToggle} />
                ))}
            </div>
        )
    }
}

export default Tasks

header

import Button from './button'
import { Component } from 'react'

class Header extends Component {
    render() {
        const {title, showAdd, onAdd} = this.props
        return (
            <header className="header-task">
                <h1>{title}</h1>
                <Button color={showAdd ? 'red' : 'green'} text={showAdd ? 'Close' : 'Add'} onClick={onAdd} />
            </header>
        )
    }
}

export default Header

button

import { Component } from "react"

class Button extends Component {
    render() {
        const { color, text, onClick } = this.props
        return (
            <button onClick={onClick} style={{ backgroundColor: color }} className="btn-task">{text}</button>
        )
    }
}

export default Button

add task

import { Component } from "react"

class AddTask extends Component {
    constructor(props) {
        super(props)

        this.state = {
            title: "",
            day: "",
            reminder: false
        }

        this.onSubmit = this.onSubmit.bind(this)
    }

    onSubmit(e) {
        e.preventDefault()

        if (this.state.title === "") {
            alert('Please add a task')
            return
        }

        this.props.onAdd({ title: this.state.title, day: this.state.day, reminder: this.state.reminder })

        this.setState({
            title: "",
            day: "",
            reminder: false
        })
    }

    render() {
        return (
            <form className="form-add-task" onSubmit={(e) => this.onSubmit(e)}>
                <div className="form-control-task">
                    <label>Task</label>
                    <input type="text" placeholder="Add Task" value={this.state.title} onChange={(e) => this.setState({title: e.target.value})} />
                </div>
                <div className="form-control-task">
                    <label>Day & Time</label>
                    <input type="text" placeholder="Add Day & Time" value={this.state.day} onChange={(e) => this.setState({day: e.target.value})} />
                </div>
                <div className="form-control-task form-control-check">
                    <label>Set Reminder</label>
                    <input type="checkbox" value={this.state.reminder} checked={this.state.reminder} onChange={(e) => this.setState({reminder: e.currentTarget.checked})} />
                </div>

                <input type="submit" value="Save Task" className="btn-task-block" />
            </form>
        )
    }
}

export default AddTask

task style css

@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400&display=swap');

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: 'Poppins', sans-serif;
}

.div-container {
  max-width: 500px;
  margin: 30px auto;
  overflow: auto;
  min-height: 300px;
  border: 1px solid steelblue;
  padding: 30px;
  border-radius: 5px;
}

.header-task {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.btn-task {
  display: inline-block;
  background: #000;
  color: #fff;
  border: none;
  padding: 10px 20px;
  margin: 5px;
  border-radius: 5px;
  cursor: pointer;
  text-decoration: none;
  font-size: 15px;
  font-family: inherit;
}

.btn-task:focus {
  outline: none;
}

.btn-task:active {
  transform: scale(0.98);
}

.btn-task-block {
  display: block;
  width: 100%;
}

.task {
  background: #f4f4f4;
  margin: 5px;
  padding: 10px 20px;
  cursor: pointer;
}

.task.reminder {
  border-left: 5px solid green;
}

.task h5 {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.form-add-task {
  margin-bottom: 40px;
}

.form-control-task {
  margin: 20px 0;
}

.form-control-task label {
  display: block;
}

.form-control-task input {
  width: 100%;
  height: 40px;
  margin: 5px;
  padding: 3px 7px;
  font-size: 17px;
}

.form-control-check {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.form-control-check label {
  flex: 1;
}

.form-control-check input {
  flex: 2;
  height: 20px;
}

footer {
  margin-top: 30px;
  text-align: center;
}

컴포넌트 부분은 다른분이 함수형으로 만들어놓은거 class형으로 만들어놓은거라

 

딱히 중요하거나 어려웠던 포인트는 없었던것 같으니 자세한 설명은 생략합니다

 

마지막으로 구현된 화면 영상입니다

 

< 첫 작품..? >

반응형
Comments