728x90

[관리자 페이지] Admin Page - 회원가입 구현하기 #2 API Router

[관리자 페이지] Admin Page - 회원가입 구현하기 #1 User Table 생성

Fast API는 include_router 를 통해서 api router 를 등록하여 prefix, global DI 등을 사용할 수 있습니다.

pip install fastapi

fastapi를 설치해 주고

root 디렉토리의 main.py 의 코드를 아래와 같이 수정합니다.

# main.py
from fastapi import FastAPI

app = FastAPI()

CLI 또는 IDE Run 으로 실행하고 localhost:4883/docs 로 접속해보면

화면을 만날 수 있습니다.

1. API 목록


현재 필요한 api 목록은 최소 2개입니다.

api 디렉토리를 만들어 주고 api_main 이라는 파일을 만들어 router를 정의해 줍니다.

.
┣━api
┃  ┣━__init__.py
┃  ┣━api_main.py
┣━database
# api/api_main.py
from typing import Optional

from fastapi import APIRouter

api_main = APIRouter()


@api_main.post("/signin")
def signin(userId: Optional[str], userPw: Optional[str]):
    return {"accessToken": "accessToken", "refreshToken": "refreshToken"}


@api_main.post("signup")
def signup(userId: Optional[str], userPw: Optional[str]):
    return {"message": "success"}

main.py 에는 api_main을 import 시키고 router를 include 합니다.

# main.py
from fastapi import FastAPI

from api import api_main

app = FastAPI(title="hiioAdminServer")

app.include_router(api_main)

다시 localhost:4883/docs 에 접속합니다.

API 가 생긴걸 볼 수 있습니다.

3. Post Method Request Model

pydentic 패키지를 이용해 Request로 들어오는 파라미터를 정의해서 사용해 줍니다.

.
┣━api
┃  ┣━models
┃  ┃ ┣━__init__.py
┃  ┃ ┣━user.py
┃  ┃
┃  ┣━__init__.py
┃  ┣━api_main.py
┣━database

api 디렉토리 안에 user 파일을 만들고

pydentic 의 BaseModel을 상속받는 UserModel class를 만들어 줍니다. 속성은 database에 정의된 id, password로 동일하게 만들어 줍니다.

# api/models/user.py
from typing import Optional

from pydantic import BaseModel, Field


class UserModel(BaseModel):
    id: Optional[str] = Field(None, description="user's id")
    password: Optional[str] = Field(None, description="user's password")

    class Config:
        from_attritues = True

class Config:
from_attributes 를 사용하면 기존의 orm_mode=True를 대체해서 사용할 수 있습니다.

아까 작성했던 signin 과 signup 함수의 파라미터를 user:User로 변경해 줍니다.


# api/api_main.py
from typing import Optional

from fastapi import APIRouter

from api.models import User

api_main = APIRouter()


@api_main.post("/signin")
def signin(user:UserModel):
    return {"accessToken": "accessToken", "refreshToken": "refreshToken"}


@api_main.post("signup")
def signup(user:UserModel):
    return {"message": "success"}

변경 된 Request body로 나오는 것을 볼 수 있습니다.

 

 

728x90
728x90

[관리자 페이지] Admin Page - 회원가입 구현하기 #1


아주 간단한 회원 가입을 위해 먼저 DB -> Server -> Front 순으로 구현해봅니다.

DB 는 MariaDB 11

Server는 FastAPI

Front는 NextJS

를 사용합니다.

Python ORM 라이브러리인 sqlAlchemy 로 데이터를 조회/등록/수정/삭제합니다.

1. User Table 만들기


회원 정보는 USER_ID 와 PASSWORD 만을 가지고 있습니다.

MariaDB에 어드민 페이지를 위한 데이터베이스와 User를 만들고 권한을 부여합니다.

프로젝트 이름은 HIIO_ADMIN 으로 정했습니다.(지금)

DB NAME 은 HIIO_ADMIN
USER 는 hiio420으로 생성합니다.


CREATE DATABASE HIIO_ADMIN;
CREATE USER 'hiio420'@'%' INDENTIFIED BY '<password>';
GRANT ALL PRIVILEGES ON HIIO_ADMIN.* TO 'hiio420'@'%' IDENTIFIED BY '<password>';

sql 쿼리로 테이블을 만들수도 있지만
sqlAlchemy를 이용해 봅니다.

디렉토리 구조는 아래와 같습니다.

.
┣━database

┃  ┣━entity
┃     ┣━__init__.py
┃     ┣━base.py
┃     ┣━user.py
┃  ┣━__init__.py
┃  ┣━core.py
┃
┣━README.md
┣━.gitignore

pip 로 mariadb와 sqlAlchemy를 설치합니다.

pip install mariadb
pip install sqlAlchemy

mariadb는 DB 연결에 쓰입니다.

core.py에 DB 연결을 위한 URL을 생성하고 create_engine method로 engine을 만들어 줍니다.

entity 디렉토리에는 DB entity 관련 class 들을 위치 시킵니다.base.py 에는 DeclarativeBase 상속받는 Base class를 만들고 user.py에는 Base class를 상속받는 User class를 만들어서 create_all()로 테이블을 생성해 줍니다.

# database/core.py
from sqlalchemy import URL, create_engine

url = URL.create(drivername="mariadb+mariadbconnector", username="hiio420", password="...", host="...",
                 port="...", database="HIIO_ADMIN")

engine = create_engine(url)

# database/entity/base.py
from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
    pass
# database/entity/user.py
from sqlalchemy import Column, String

from database.entity.base import Base


class User(Base):
    __tablename__ = "USER"
    __table_args__ = {
        'comment': 'USER TABLE'
    }
    id = Column("USER_ID", String(20), primary_key=True, comment="USER ID")
    password = Column("PASSWORD", String(120), nullable=False, comment="USER PASSWORD")

root 디렉토리에 main.py 를 만들어서 실행 시켜 보면 User 테이블이 만들어 진 것을 볼 수 있습니다.

# main.py
from database import engine, Base

if __name__ == '__main__':
    Base.metadata.drop_all(engine)
    Base.metadata.create_all(engine)

728x90
728x90

[관리자 페이지] Admin Page - 로그인 프로세스 생각해보기 #2


User는 먼저 로그인 화면으로 진입합니다. 로그인 할 수 있는 아이디나 패스워드가 없는 경우 회원가입을 진행하고 회원가입이 정상적으로 완료되면 다시 로그인 화면으로 돌아가서 로그인을 진행 성공하면 메인 페이지로 이동하는 프로세스를 생각해 봅니다.


flowchart LR
    User-->SignIn

        SignIn-->|Don't Have credential|SignUp

        subgraph SignUp Process
        SignUp --->|Fail| Alert1[Alert SignIn fail]
        end

    subgraph SignIn Process
         SignIn ===>|Success| Main
        SignIn--->|Fail| Alert2[Alert SignIn fail]
        end

        SignUp ===>|success| SignIn

728x90
728x90

Admin Page - 로그인 프로세스 생각해보기


User가 Admin Page에 접근했을 경우

  1. User의 Token 이 존재 하는지 확인
  2. 존재 한다면 Server에서 Token 발송 검증 요청
  3. User 정보 return
  4. Token 이 없다면 Sign In Page로 이동

위 순서로 접근한다고 하고 아래와 같은 Sequence Diagram을 생각해 봤다.

Diagram

sequenceDiagram
        autonumber
        actor User
    participant Admin Main
    participant Admin Server
    participant Admin DB

    User ->>+ Admin Main: Access Admin Main Page
        alt Token exists === true
            Admin Main ->>+ Admin Server: Request User Info WIth Token


            rect rgba(0, 23, 255, .1)
                alt Token valid Ok 

                    Admin Server ->>+ Admin DB : Select User by UserId
                    Activate Admin DB

                    rect rgba(0, 56, 255, .1)
                        alt User exists Ok
                            Admin DB ->>- Admin Server:Return User Info
                            Activate Admin Server
                            Admin Server ->>- Admin Main:Return User Info
                            Activate Admin Main
                            Admin Main ->>- User : Data
                        else User not exists
                            Admin DB ->>- Admin Server:Return None
                            Activate Admin Server
                            Admin Server ->>- Admin Main : Return 404 & Data is None
                            Activate Admin Main 
                            Admin Main ->>- User : Alert Msg
                        end
                    end
                else Token Valid False
                    Admin Server ->>- Admin Main : return 401
                    Activate Admin Main
                    Admin Main ->>- User : Token Valid Fail Redirect Admin Sign In
                end
            end
            else Token not Exists
                Admin Main ->>- User : Token Not Exists & Redirect Admin Sign In
        end

위 프로세스를 토대로 13 개의 요구사항을 정의 해 보았습니다.

이를 토대로 개발에 들어가면 좋을지 고민해 봅니다.

아직은 어설프지만 다이어그램이나 요구사항을 만들어 봤다는 것의 의의를 둡니다.

User 접근에 대한 개발을 하기에 앞서 아무런 User 정보가 없기 때문에 User 정보를 입력받는 Sign Up 과 로그인하는 Sign In 프로세스에 대한 고민을 좀더 해봐야 할 거 같습니다.

728x90
728x90

Next.js 컴포넌트화


현재 Front 쪽 코드는 아래와 같습니다.

import {FormEventHandler} from "react";
import {redirect} from "next/navigation";


export default async function Home() {


    async function getData(){
        "use server";
        const url = new URL("http://localhost:4882")
        Object.keys(searchParams).map((param:string)=>{
            url.searchParams.append(param,searchParams[param] as string)
        });
        let resp = await fetch(url);
        let result = await resp.json()
        return result.data
    }

    const items = await getData();
    return (
        <main className="flex min-h-screen flex-col items-center justify-between p-24">
            <form className={"w-full overflow-hidden"}>
                <div className="pt-2 relative text-gray-600 w-px245 w-max">
                    <input
                        className="border-2 border-gray-300 bg-white h-10 px-5 pr-16 rounded-lg text-sm focus:outline-none relative"
                        type="search" name="srchTxt" placeholder="Search"/>
                    <button type="submit" className="absolute right-0 top-0 mt-5 mr-4">
                        <svg className="text-gray-600 h-4 w-4 fill-current" xmlns="http://www.w3.org/2000/svg"
                             xmlnsXlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px"
                             viewBox="0 0 56.966 56.966" style={{background: "new 0 0 56.966 56.966"}}
                             xmlSpace="preserve"
                             width="512px" height="512px">
                            <path
                                d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23  s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92  c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17  s-17-7.626-17-17S14.61,6,23.984,6z"/>
                        </svg>
                    </button>
                </div>

                <div className="flex flex-col">
                    <div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
                        <div className="inline-block min-w-full py-2 sm:px-6 lg:px-8">
                            <div className="overflow-hidden">
                                <table
                                    className="min-w-full text-left text-sm font-light text-surface dark:text-white">
                                    <thead
                                        className="border-b border-neutral-200 font-medium dark:border-white/10">
                                    <tr>
                                        <th scope="col" className="px-6 py-4">#</th>
                                        <th scope="col" className="px-6 py-4">First</th>
                                        <th scope="col" className="px-6 py-4">Last</th>
                                        <th scope="col" className="px-6 py-4">Handle</th>
                                    </tr>
                                    </thead>
                                    <tbody>
                                    {items?.map((item:{[key:string]:string})=> {
                                        return <tr className="border-b border-neutral-200 dark:border-white/10" key={Math.random()}>
                                            <td className="whitespace-nowrap px-6 py-4 font-medium">{item["번호"]}</td>
                                            <td className="whitespace-nowrap px-6 py-4">{item[Object.keys(item)[1]]}</td>
                                            <td className="whitespace-nowrap px-6 py-4">{item[Object.keys(item)[2]]}</td>
                                            <td className="whitespace-nowrap px-6 py-4">{item[Object.keys(item)[3]]}</td>
                                        </tr>
                                    })}
                                    </tbody>
                                </table>
                            </div>
                        </div>
                    </div>
                </div>

                <nav aria-label="Page navigation example">
                    <ul className="list-style-none flex">
                        <li>
                            <a
                                className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none focus:ring-0 active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                                href="#"
                            >Previous</a
                            >
                        </li>
                        <li>
                            <a
                                className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                                href="#"
                            >1</a
                            >
                        </li>
                        <li aria-current="page">
                            <a
                                className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                                href="#"
                            >2</a
                            >
                        </li>
                        <li>
                            <a
                                className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                                href="#"
                            >3</a
                            >
                        </li>
                        <li>
                            <a
                                className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                                href="#"
                            >Next</a
                            >
                        </li>
                    </ul>
                </nav>
            </form>
        </main>
    );
}

 

response 받은 데이터를 기반으로 컴포넌트들을 그려보면 좋을 거 같습니다.

 

src 디렉토리에 libs 디렉토리를 만들고 그 하위에 components 를 만들어 줍니다.

 

처음으로 만들 컴포넌트는 3개입니다.

 

1. 검색 바

2. 데이터 리스트

3. paging navigation

 

먼저 각 부분 디렉토리를 만들어 주고, 그안에 index.tsx 그리고 컴포넌의 tsx 파일을 만들고 코드를 옮겨줍니다.

 

 

1. 검색 바 - SearchBar


export default function SearchBar({searchText}:{searchText:string}) {

    return <div className="pt-2 relative text-gray-600 w-px245 w-max">
        <input
            className="border-2 border-gray-300 bg-white h-10 px-5 pr-16 rounded-lg text-sm focus:outline-none relative"
            type="search" name="srchTxt" placeholder="Search" defaultValue={searchText || ""}/>
        <button type="submit" className="absolute right-0 top-0 mt-5 mr-4">
            <svg className="text-gray-600 h-4 w-4 fill-current" xmlns="http://www.w3.org/2000/svg"
                 xmlnsXlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px"
                 viewBox="0 0 56.966 56.966" style={{background: "new 0 0 56.966 56.966"}}
                 xmlSpace="preserve"
                 width="512px" height="512px">
                <path
                    d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23  s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92  c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17  s-17-7.626-17-17S14.61,6,23.984,6z"/>
            </svg>
        </button>
    </div>
}

 

 

검색 바의 경우에는 기본 value 값이 필요합니다. 검색 입력 된 텍스트를 기본 value로 넣어 주는데, Next.js의 경우에는 defaultValue에 넣어 줍니다.

 

2. DataList


type item = { [key: string]: string }

type PropsType = {
    items?: item[]
}

export default function DataList({items}:PropsType) {
    return <div className={"grow"}>
        <div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
            <div className="inline-block min-w-full py-2 sm:px-6 lg:px-8 grow-1">
                <div className="overflow-hidden">
                    <table
                        className="min-w-full text-left text-sm font-light text-surface dark:text-white">
                        <thead
                            className="border-b border-neutral-200 font-medium dark:border-white/10">
                        <tr>
                            <th scope="col" className="px-3 sm:px-6 py-3 sm:py-4">#</th>
                            <th scope="col" className="px-3 sm:px-6 py-3 sm:py-4">First</th>
                            <th scope="col" className="px-3 sm:px-6 py-3 sm:py-4">Last</th>
                            <th scope="col" className="px-3 sm:px-6 py-3 sm:py-4">Handle</th>
                        </tr>
                        </thead>
                        <tbody>
                        {items?.map((item: { [key: string]: string }) => {
                            return <tr className="border-b border-neutral-200 dark:border-white/10"
                                       key={Math.random()}>
                                <td className="whitespace-nowrap px-3 sm:px-6 py-3 sm:py-4 font-medium">{item["번호"]}</td>
                                <td className="whitespace-nowrap px-3 sm:px-6 py-3 sm:py-4">{item[Object.keys(item)[1]]}</td>
                                <td className="whitespace-nowrap px-3 sm:px-6 py-3 sm:py-4">{item[Object.keys(item)[2]]}</td>
                                <td className="whitespace-nowrap px-3 sm:px-6 py-3 sm:py-4">{item[Object.keys(item)[3]]}</td>
                            </tr>
                        })}
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
}

 

 

 

3. PagingNav


import {PagingType} from "./utils.d"

export class Paging {

    pagingSize: number = 10;
    curPagingSize: number = 10;
    curFirstPage: number = 1;
    curLastPage: number = 0;
    curPrevPage: number = 0;
    curNextPage: number = 0;
    lastPage: number = 0;
    curPrevPageOk: boolean = false;
    curNextPageOk: boolean = false;

    constructor(paging: PagingType) {
        this.pagingSize = paging?.pagingSize || this.pagingSize;
        this.curFirstPage = this.getCurFirstPage(paging.page);
        this.curLastPage = this.getCurLastPage(paging.page, paging.totalPage);
        this.curPagingSize = this.curLastPage - this.curFirstPage + 1;
        this.curPrevPage = paging.page - 1;
        this.curNextPage = paging.page + 1;
        this.curPrevPageOk = 0 <= this.curPrevPage;
        this.curNextPageOk =this.curNextPage < paging.totalPage;
        this.lastPage = paging.totalPage;
    }

    getCurLastPage(page: number, totalPage: number): number {
        let curLp: number = this.getCurFirstPage(page) + (this.pagingSize - 1);
        return curLp >= totalPage ? totalPage : curLp;
    }

    getCurFirstPage(page: number): number {
        return Math.floor((page - 1) / this.pagingSize) * this.pagingSize + 1;
    }
}
import {PagingType} from "@/libs/utils/utils.d";
import {Paging} from "@/libs/utils/utils";

type PropsType = {
    paging:PagingType
}

export default function PagingNav({paging}:PropsType) {

    const page = new Paging(paging);
    return <nav aria-label="Page navigation example">
        <ul className="list-style-none flex">
            {page.curPrevPageOk && <>
                <li>
                    <button
                        className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none focus:ring-0 active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                        type={"submit"} value={1} name={"page"}
                    >First
                    </button
                    >
                </li>
                <li>
                    <button
                        className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none focus:ring-0 active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                        type={"submit"} value={page.curPrevPage} name={"page"}
                    >Previous
                    </button
                    >
                </li>

            </>}
            {Array(page.curPagingSize).fill(0).map((_, i) => {
                return <li>
                    <button
                        className={`relative block rounded px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 hover:text-black focus:bg-neutral-100 focus:text-primary-700 focus:outline-none active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500 ${paging.page === page.curFirstPage + i && "bg-black text-white"}`}
                        type={"submit"} value={page.curFirstPage + i} name={"page"}
                    >{page.curFirstPage + i}</button>
                </li>
            })}
            {page.curNextPageOk && <>
                <li>
                    <button
                        className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                        type={"submit"} value={page.curNextPage} name={"page"}
                    >Next
                    </button>
                </li>
                <li>
                    <button
                        className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                        type={"submit"} value={page.lastPage} name={"page"}
                    >Last
                    </button>
                </li>
            </>}
        </ul>
    </nav>
}

 

 

4.Page


import {FormEventHandler} from "react";
import {redirect} from "next/navigation";
import {Paging} from "../libs/utils/utils";
import SearchBar from "@/libs/components/SearchBar/SearchBar";
import DataList from "@/libs/components/DataList/DataList";
import PagingNav from "@/libs/components/PagingNav/PagingNav";

type searchParamsType = { [key: string]: string | null }
export default async function Home({searchParams}: { searchParams: searchParamsType }) {

    async function getData(searchParam?: searchParamsType) {
        "use server";
        const url = new URL("http://localhost:4882")
        Object.keys(searchParams).map((param: string) => {
            url.searchParams.append(param, searchParams[param] as string)
        });
        let resp = await fetch(url, {cache:"no-store"});
        // let resp = await fetch(url, {next:{revalidate:60*60}});
        let result = await resp.json()
        return result
    }

    async function go(formData: FormData = new FormData()) {
        "use server";
        let curPage:string | null = searchParams["page"]
        let curSrchTxt:string | null = searchParams["srchTxt"]
        let page:FormDataEntryValue | null = formData.get('page')
        const url = new URL("http://localhost:3000")
        url.searchParams.append("srchTxt", formData.get('srchTxt') as string);
        if(curPage && !page){
            url.searchParams.append("page", curPage as string);
        }else if(page){
            url.searchParams.append("page", page as string);
        }

        if(curSrchTxt===formData.get('srchTxt')as string && curPage===page){
            return false;
        }

        redirect(url.toString());

    }

    const result = await getData(searchParams);
    const {items, paging} = result;

    return (
        <main className="flex min-h-screen flex-col items-center justify-between p-6 sm:p-24 h-screen">
            <form action={go} className={"w-full flex flex-col grow"}>

                <SearchBar searchText={searchParams["srchTxt"] as string}/>

                <DataList items={items}/>
                <PagingNav paging={paging} />
            </form>
        </main>
    );
}

 

728x90
728x90

패키지 업그레이드 및 업데이트

sudo apt-get update
sudo apt-get upgrade

 

vsftpd 설치

sudo apt-get install vsftpd

 

설정 변경

sudo vim /etc/vsftpd.conf
local_enable=YES
write_enable=YES
local_umask=022
chroot_local_user=YES

utf8_filesystem=YES

# port 변경
listen_port=45620  
pasv_min_port=45621
pasv_max_port=45622

 

 

SSL/TLS

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/vsftpd.pem -out /etc/ssl/private/vsftpd.pem



ssl_enable=YES
ssl_tlsv1=YES
implicit_ssl=YES
rsa_cert_file=/etc/ssl/private/vsftpd.pem
rsa_private_key_file=/etc/ssl/private/vsftpd.pem
728x90

'모음집 > Linux' 카테고리의 다른 글

[Ubuntu 20.04] MariaDB 11.3.2 설치  (4) 2024.03.12
[Linux] 리눅스 명령어 모음집 #1 / 우분투 /  (0) 2022.01.20
728x90

FastAPI ResponseBody Model 만들기


https://fastapi.tiangolo.com/ko/tutorial/response-model/

 

응답 모델 - FastAPI

FastAPI framework, high performance, easy to learn, fast to code, ready for production

fastapi.tiangolo.com

 

 

1. ResponseModel

 

from typing import Optional, List

from pydantic import Field, BaseModel


class ResponseModel(BaseModel):
    msg:str = Field("", description="응답메세지")
    isOk:bool =  Field(True, description="응답 성공 여부")
    items:Optional[List[dict] | dict] = Field(None, description="응답 데이터")

 

 

2. ReponseModelPaging

 

class Paging(BaseModel):
    totalSize: int = Field(0, description="전체 갯수")
    totalPage: int = Field(0, description="전체 페이지 수")
    page: int = Field(1, description="현재 페이지")
    size: int = Field(10, description="출력 갯수")


class ResponseModelPaging(ResponseModel):
    paging: Paging = Field(Paging(), description="페이지 정보")

 

 

3. Genric Type

from typing import TypeVar, Generic
T = TypeVar("T")

class ResponseModel(BaseModel,Generic[T]):

 

 

4. Main

"""
main.py
@제목: 메인 실행 파일
@설명: 메인 실행 파일

    작성일자        작성자
-----------------------
    2024.03.14    hiio420

"""
import math
import os.path

from fastapi import FastAPI
from pydantic import BaseModel
from starlette.middleware.cors import CORSMiddleware

from libs import ExcelUtils
from models import ResponseModel, ResponseModelPaging, Paging

# 엑셀 파일 불러오기

CUR_DIR = os.path.abspath(os.path.dirname(__file__))
UPLOAD_DIR = os.path.join(CUR_DIR, 'upload')
file_path = os.path.join(UPLOAD_DIR, '(붙임)공공데이터 공통표준용어(2022.7월).xlsx')

excel_utils = ExcelUtils()
df_dict = excel_utils.read_excel(file_path)

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


class Item(BaseModel):
    data_id: int = 0
    page: int = 1
    limit: int = 10


@app.get("/",response_model=ResponseModelPaging[dict])
def read_root(dataId: int = 0, page: int = 1, limit: int = 10, srchTxt: str = ""):
    if page < 1:
        page = 0
    if limit < 10:
        limit = 10
    df = df_dict[dataId]
    df = df[df.apply(lambda row: row.astype(str).str.contains(srchTxt, case=False).any(), axis=1)]

    first_idx = (page - 1) * limit
    last_idx = first_idx + limit - 1
    total_size = df.shape[0]
    last_page = math.ceil(total_size / limit)

    df_sliced = df[first_idx:last_idx + 1].copy()

    df_sliced["번호"] = [i for i in
                       range(total_size - (page - 1) * limit, total_size - (page - 1) * limit - df_sliced.shape[0], -1)]
    data = []
    if df_sliced.shape[0] != 0:
        data = df_sliced.to_dict("records")
    paging = Paging(page=page, size=limit, totalPage=last_page,totalSize=total_size)
    resp = ResponseModelPaging(items=data,paging=paging)
    print(resp)
    return resp
728x90
728x90

화면 만들어보기 #3 fetch 로 데이터 불러와서 뿌려주기


이제 이전에 만들었던 FastAPI 서버와 통신해 데이터를 가져와보자.

 

NextJs 는 13버전 에서 부터 SSG, SSR, ISR의 Data Fetching 을 fetch api 를 통해 사용 할 수 있게 되었다.

 

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating

 

Data Fetching: Fetching, Caching, and Revalidating | Next.js

Learn how to fetch, cache, and revalidate data in your Next.js application.

nextjs.org

 

 

그렇다고 합니다.

 

우선은 깊게 알아보지는 않고 fetch 함수를 이용해 데이터를 불러옵니다.

 

export default async function Home() {

    const response =await fetch("http://localhost:4882");
    const data =await response.json();
    const items = data.data;
    console.info(items.length)
    
    ...

 

데이터가 잘 들어오는 것을 볼 수 있습니다.

 

이제 이 데이터를 가지고 이전에 그렸던 Data Table 부분을 처리해봅니다.

 

 

{items?.map((item:{[key:string]:string})=> {
    return <tr className="border-b border-neutral-200 dark:border-white/10" key={Math.random()}>
        <td className="whitespace-nowrap px-6 py-4 font-medium">{item["번호"]}</td>
        <td className="whitespace-nowrap px-6 py-4">{item[Object.keys(item)[1]]}</td>
        <td className="whitespace-nowrap px-6 py-4">{item[Object.keys(item)[2]]}</td>
        <td className="whitespace-nowrap px-6 py-4">{item[Object.keys(item)[3]]}</td>
    </tr>
})}

 

데이터가 잘 나오기는 하지만 아직 어영부영 어수선 해 보이네요 무엇을 중점적으로 볼지도 모르고 아직은 많이 부족합니다.

 

우선은 스타일 조정 부터 해줍니다.

 

form의 width 부터 main 태그에 맞춰 추기 위해 className 에 w-full을 주었습니다.

 

<form className={"w-full"}>
...

 

 

돋보기 아이콘이 search box 안에 있어야하는데 searchbox를 감사는 div의 너비가 form에 맞춰지면서 범위를 벗어나 버렸습니다.

 

div 너비를 width:max-content 값을 줘서 맞춰줍니다.

<div className="pt-2 relative mx-auto text-gray-600 w-px245 w-max">
...

 

 

 

가운데 있는 것보다는 왼쪽으로 정렬된 것이 보기 좋을 거 같아서 mx-auto는 지워버립니다.

 

<div className="pt-2 relative text-gray-600 w-px245 w-max">
...

 

얼추 만들어져 가는 모습이네요 

 

 

728x90
728x90

화면 만들어보기 #2 기본 화면 그려보기


 

이제 처음으로 화면을 그려 보자!!!!!

화면을 직접 구현해 보는건 언제나 많은 고민이 따르는 듯

 

 

매우 심플한 레이아웃을 가지고 있다.

 

이제 각 부분에 한땀한땀 디자인을 넣는것 보다 구글링해서 찾은 이쁜 Tailwind Style 에 컴포넌트를 넣어보자

 

 

Search Bar 의 경우

https://tailwindcomponents.com/component/search-bar

 

Search bar | Forms

'Gray outline, navigation, minimal, search, tailwind css'

tailwindcomponents.com

를 참고 하였다.

 

<!-- component -->
<!-- This is an example component -->
     <div class="pt-2 relative mx-auto text-gray-600">
        <input class="border-2 border-gray-300 bg-white h-10 px-5 pr-16 rounded-lg text-sm focus:outline-none"
          type="search" name="search" placeholder="Search">
        <button type="submit" class="absolute right-0 top-0 mt-5 mr-4">
          <svg class="text-gray-600 h-4 w-4 fill-current" xmlns="http://www.w3.org/2000/svg"
            xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px"
            viewBox="0 0 56.966 56.966" style="enable-background:new 0 0 56.966 56.966;" xml:space="preserve"
            width="512px" height="512px">
            <path
              d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23  s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92  c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17  s-17-7.626-17-17S14.61,6,23.984,6z" />
          </svg>
        </button>
      </div>

 

 

이 코드를 바로 쓰기에는 문법에 다 걸리기 때문에 몇몇 부분을 바꿔주어야 한다.

 

class -> className

 

xmlns:xlink -> xmlnsXlink

 

xml:space -> xmlSpace

 

style="..." => style={{...}}

 

수정을 해주고 npm run dev를 실행하면 검색 Input이 나타 난다.

 

 

 

 

 

Data List View 는 

https://tw-elements.com/docs/standard/data/tables/

 

Tailwind CSS Tables - Free Examples & Tutorial

Use responsive table component, with helper examples for table column width, bordered and striped tables, pagination, fixed header, overflow, spacing & more.

tw-elements.com

사이트를 참고 하였고, 가장 심플한 부분을 사용했다.

 

Pagination 도 위 사이트에서 참고 하였다.

 

https://tw-elements.com/docs/standard/navigation/pagination/

 

Tailwind CSS Pagination - Free Examples & Tutorial

Use responsive pagination component with helper examples for buttons, arrows, previous & next pages. Use pagination in tables or with long content.

tw-elements.com

 

위 3가지 컴포넌트를 우선 form 태그로 감싸주자.

 

export default function Home() {

    return (
        <main className="flex min-h-screen flex-col items-center justify-between p-24">
            <form>
                <div className="pt-2 relative mx-auto text-gray-600">
                    <input
                        className="border-2 border-gray-300 bg-white h-10 px-5 pr-16 rounded-lg text-sm focus:outline-none"
                        type="search" name="search" placeholder="Search"/>
                    <button type="submit" className="absolute right-0 top-0 mt-5 mr-4">
                        <svg className="text-gray-600 h-4 w-4 fill-current" xmlns="http://www.w3.org/2000/svg"
                             xmlnsXlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px"
                             viewBox="0 0 56.966 56.966" style={{background: "new 0 0 56.966 56.966"}}
                             xmlSpace="preserve"
                             width="512px" height="512px">
                            <path
                                d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23  s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92  c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17  s-17-7.626-17-17S14.61,6,23.984,6z"/>
                        </svg>
                    </button>
                </div>

                <div className="flex flex-col">
                    <div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
                        <div className="inline-block min-w-full py-2 sm:px-6 lg:px-8">
                            <div className="overflow-hidden">
                                <table
                                    className="min-w-full text-left text-sm font-light text-surface dark:text-white">
                                    <thead
                                        className="border-b border-neutral-200 font-medium dark:border-white/10">
                                    <tr>
                                        <th scope="col" className="px-6 py-4">#</th>
                                        <th scope="col" className="px-6 py-4">First</th>
                                        <th scope="col" className="px-6 py-4">Last</th>
                                        <th scope="col" className="px-6 py-4">Handle</th>
                                    </tr>
                                    </thead>
                                    <tbody>
                                    <tr className="border-b border-neutral-200 dark:border-white/10">
                                        <td className="whitespace-nowrap px-6 py-4 font-medium">1</td>
                                        <td className="whitespace-nowrap px-6 py-4">Mark</td>
                                        <td className="whitespace-nowrap px-6 py-4">Otto</td>
                                        <td className="whitespace-nowrap px-6 py-4">@mdo</td>
                                    </tr>
                                    <tr className="border-b border-neutral-200 dark:border-white/10">
                                        <td className="whitespace-nowrap px-6 py-4 font-medium">2</td>
                                        <td className="whitespace-nowrap px-6 py-4">Jacob</td>
                                        <td className="whitespace-nowrap px-6 py-4">Thornton</td>
                                        <td className="whitespace-nowrap px-6 py-4">@fat</td>
                                    </tr>
                                    <tr className="border-b border-neutral-200 dark:border-white/10">
                                        <td className="whitespace-nowrap px-6 py-4 font-medium">3</td>
                                        <td className="whitespace-nowrap px-6 py-4">Larry</td>
                                        <td className="whitespace-nowrap px-6 py-4">Wild</td>
                                        <td className="whitespace-nowrap px-6 py-4">@twitter</td>
                                    </tr>
                                    </tbody>
                                </table>
                            </div>
                        </div>
                    </div>
                </div>

                <nav aria-label="Page navigation example">
                    <ul className="list-style-none flex">
                        <li>
                            <a
                                className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none focus:ring-0 active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                                href="#"
                            >Previous</a
                            >
                        </li>
                        <li>
                            <a
                                className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                                href="#"
                            >1</a
                            >
                        </li>
                        <li aria-current="page">
                            <a
                                className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                                href="#"
                            >2</a
                            >
                        </li>
                        <li>
                            <a
                                className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                                href="#"
                            >3</a
                            >
                        </li>
                        <li>
                            <a
                                className="relative block rounded bg-transparent px-3 py-1.5 text-sm text-surface transition duration-300 hover:bg-neutral-100 focus:bg-neutral-100 focus:text-primary-700 focus:outline-none active:bg-neutral-100 active:text-primary-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-primary-500 dark:active:bg-neutral-700 dark:active:text-primary-500"
                                href="#"
                            >Next</a
                            >
                        </li>
                    </ul>
                </nav>
            </form>
        </main>
    );
}

 

 

이런 생각 한것과는 다르게 몇몇 부분이 생각과는 다르게 나타났다.

 

다음에 고쳐봐야지

728x90
728x90

WebStorm NextJs 설정


Front 단에 사용할 Framework는 NextJS입니다. 

이전에 React로 개발해 본 경험이 있고, NextJS에 대해 공부도 해보고자 선택하게 되었습니다.

 

Code Editor는 VScode 도 있지만 WebStorm을 사용해 보고자 합니다.

 

WebStrom에서는 새 프로젝트를 만들때 프레임워크를 선택할 수 있고, NextJS도 포함되어 있습니다.

 

TypeScript를 선택하고 만들어 줍니다.

Create 클릭하면

Terminal에 설정 부분으로 넘어 갑니다.

 

 

제가 설정한 값들은 

입니다.

 

설정값들을 선택 후에는 프로젝트에 필요한 모듈들이 설치됩니다.

 

 

 

설치가 완료되면 프로젝트 기본 파일들이 생성된 것을 볼 수 있습니다.

728x90

+ Recent posts