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

+ Recent posts