개발 시작하기

개발을 시작하기 전에

우리는 이 과정을 통해 직원 목록을 보는 페이지를 만들겁니다. ProjectRoom으로 Web Application을 만드는 기본적인 사항들을 배웁니다.

무엇을 만들게 될까요?

필요한 선수 지식

REST API, Client Side Rendering(CSR), Server Side Rendering(SSR)의 개념에 대해 익숙하다고 가정합니다.

요청은 어떻게 해야할까요?

요청을 처리할 API(Application Programming Interface)를 만들어 보겠습니다. ProjectRoom은 REST API 중 POST만을 사용하도록 설계되어있습니다.

처음 접근해야하는 파일을 알아보겠습니다.

우선, 워크스페이스의 왼쪽 트리를 확인합니다.

SERVER -> src -> main -> java -> projectroom -> module

위의 module 폴더 아래에 자바 파일을 생성하면 됩니다.

자, 그러면 Emp 자바 클래스를 만들어 보겠습니다.

package projectroom.module;

import io.projectroom.framework.vo.AppContext;
import io.projectroom.framework.vo.DataItem;
import io.projectroom.framework.annotation.Module;

import java.util.ArrayList;
import java.util.List;

@Module
public class Emp {

    public List<DataItem> getNames(DataItem parameter, AppContext appContext) {
        List<DataItem> list = new ArrayList<>();
        list.add(new DataItem().append("name", "홍길동"));
        list.add(new DataItem().append("name", "둘리"));
        return list;
    }
}

module 폴더 아래의 java 클래스 중 @Module 어노테이션이 붙은 클래스의 메소드명을 대상으로 URL Mapping이 됩니다.

아래와 같이 /api/{클래스명}/{메소드명} 형태로 URL Mapping이 이뤄집니다.

module 아래의 User의 getUserByName 메소드/함수이므로

/api/User/getUserByName로 URL이 매핑됩니다.

GET /api/User/getUserByName

테스트는 어떻게 해야할까요?

워크스페이스 우측 상단의 API Tester를 클릭하여 만들어진 API를 확인해봅니다.

  1. API Tester를 누릅니다.

  2. User를 클릭하면 해당 모듈에서 만들어둔 API 목록이 보여집니다.

  3. 테스트 하고자 하는 API를 클릭합니다.

  4. 요청에 맞는 Parameter를 넣어줍니다.

  5. Send 버튼을 눌러 API 요청을 합니다.

  6. 응답 결과가 제대로 오는지 확인합니다.

화면은 어떻게 보여줄지 알아봅시다

자, API를 만들었고, 기본적인 테스트를 진행하였습니다. 만들어진 해당 API를 클라이언트 단에선 어떻게 사용할까요?

처음 접근해야 하는 파일을 알아봅시다.

우선, 워크스페이스의 왼쪽 트리를 확인합니다. index.html 파일이 첫화면이 되는 파일입니다.

UI -> public -> index.html

현재 개발된 첫화면을 보려면 어떻게 할까요?

첫화면에 접근하기 위해선 우측 상단의 Web Tester를 클릭하면 됩니다.

아래와 같이 각각 부여된 Web Tester 주소를 통해 개발중인 화면(index.html)에 접근을 할 수 있게 됩니다

사용자 마다 Web Tester 주소가 주어집니다.

자, 이제 예제 index 페이지를 구성해보겠습니다. 아래의 코드를 넣고 복사 붙여넣기를 합니다.

/UI/public/index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />

    <title>Javascript app</title>

    <link rel="icon" type="image/png" href="./static/favicon.ico" />
    <link rel="stylesheet" href="./static/build/bundle.css" />

    <script src="https://code.jquery.com/jquery-3.6.0.min.js"
        integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous" />
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous">
    </script>

    <script src="./static/build/bundle.js"></script>
</head>

<body>
    <div class="container-fluid">
        <div class="container-xxl bd-gutter mt-3 my-md-4 bd-layout">
            <div class="bd-main order-1">
                <h2>기본적인 CRUD</h2>
                <div class="section">
                    <div class="form-row d-md-flex justify-content-md-end">
                        <button type="button" class="btn btn-primary btn-sm me-md-1" id="list-refresh">
                          <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
                            <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
                            <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
                          </svg>
                        </button>
                        <button type="button" class="btn btn-success btn-sm me-md-1" id="add-buttton">Create</button>
                        <form class="d-flex" role="search">
                            <div class="input-group">
                                <input class="form-control" id="search-area"type="search" placeholder="Search" aria-label="Search"/>
                                <button type="button" class="btn btn-outline-secondary" id="search-button">
                                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16">
                                       <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"></path>
                                    </svg>
                                </button>
                            </div>
                        </form>
                    </div>
                    <div class="modal fade" id="add-modal" tabindex="-1" aria-labelledby="add-modal-label"
                        aria-hidden="true">
                        <div class="modal-dialog">
                            <div class="modal-content">
                                <div class="modal-header">
                                    <h5 class="modal-title" id="add-modal-label">
                                        사용자 추가
                                    </h5>
                                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
                                </div>
                                <div class="modal-body">
                                    <div class="mb-3 row">
                                        <label for="inputPassword" class="col-sm-2 col-form-label user-name">이름</label>
                                        <div class="col-sm-10">
                                            <input type="text" class="form-control" name="name" />
                                        </div>
                                    </div>
                                    <div class="mb-3 row">
                                        <label for="inputPassword" class="col-sm-2 col-form-label user-email">이메일</label>
                                        <div class="col-sm-10">
                                            <input type="text" class="form-control" name="email" />
                                        </div>
                                    </div>
                                </div>
                                <div class="modal-footer">
                                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                                    <button type="button" class="btn btn-primary" id="modal-add-button">Save changes</button>
                                </div>
                            </div>
                        </div>
                    </div>
                    <div class="modal fade" id="modify-modal" tabindex="-1" aria-labelledby="modify-modal-label"
                        aria-hidden="true">
                        <div class="modal-dialog">
                            <div class="modal-content">
                                <div class="modal-header">
                                    <h5 class="modal-title" id="modify-label">사용자 변경</h5>
                                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                                </div>
                                <div class="modal-body">
                                    <div class="mb-3 row">
                                        <label for="inputId" class="col-sm-2 col-form-label user-id">아이디</label>
                                        <div class="col-sm-10">
                                            <input type="text" class="form-control" name="id" disabled readonly/>
                                        </div>
                                    </div>
                                    <div class="mb-3 row">
                                        <label for="inputPassword" class="col-sm-2 col-form-label user-name">이름</label>
                                        <div class="col-sm-10">
                                            <input type="text" class="form-control" name="name" />
                                        </div>
                                    </div>
                                    <div class="mb-3 row">
                                        <label for="inputPassword" class="col-sm-2 col-form-label user-email">이메일</label>
                                        <div class="col-sm-10">
                                            <input type="text" class="form-control" name="email" />
                                        </div>
                                    </div>
                                </div>
                                <div class="modal-footer">
                                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                                    <button type="button" class="btn btn-primary" id="modal-modify-button">Save changes</button>
                                </div>
                            </div>
                        </div>
                    </div>
                    <table class="table">
                        <thead>
                            <tr>
                                <th scope="col">ID</th>
                                <th scope="col">Name</th>
                                <th scope="col">Email</th>
                                <th scope="col"></th>
                            </tr>
                        </thead>
                        <tbody id="list-area" />
                    </table>
                </div>
                <h2>파일 업로드</h2>
                <div class="section">
                    <div class="input-group">
                        <input type="file" class="form-control" id="upload-area" aria-describedby="inputGroupFileAdd" aria-label="Upload" multiple/>
                        <button class="btn btn-outline-secondary" type="button" id="upload-button">Upload File</button>
                    </div>
                    <table class="table">
                        <thead>
                            <tr>
                                <th scope="col">File Name</th>
                                <th scope="col">Size</th>
                            </tr>
                        </thead>
                        <tbody id="file-area" />
                    </table>
                </div>
            </div>
        </div>
    </div>
</body>

</html>

index.html의 정적인 부분을 채워 넣었으니, 이제 동적인 작업을 해줄 index.js 부분도 마저 작업해봅시다.

index.html 페이지가 제대로 나오고 있는지 확인합니다.

시작점이 되는는 index.html 페이지가 제대로 나온다면, 아래의 javascript 코드를 만들어봅시다.

/UI/src/index.js
import "./index.css";
import CRUD from "./crud.js";
import Upload from "./upload.js";

$(function () {
    const crudData = {
        listRefresh: $("#list-refresh"),
        addButton: $("#add-buttton"),
        searchArea: $("#search-area"),
        searchButton: $("#search-button"),
        listArea: $("#list-area"),
        addModal: $("#add-modal"),
        modifyModal: $("#modify-modal"),
        modalAddButton: $("#modal-add-button"),
        modalModifyButton: $("#modal-modify-button"),
    };
    new CRUD(crudData);

    const uploadData = {
        uploadArea: $("#upload-area"),
        uploadButton: $("#upload-button"),
        fileArea: $("#file-area"),
    };

    new Upload(uploadData);
});
/UI/src/crud.js
import { PRConnect } from "@projectroom/ui-lib";

export default class CRUD {
    constructor({
        listRefresh,
        addButton,
        searchArea,
        searchButton,
        listArea,
        addModal,
        modifyModal,
        modalAddButton,
        modalModifyButton,
    }) {
        this.listRefresh = listRefresh;
        this.addButton = addButton;
        this.searchArea = searchArea;
        this.searchButton = searchButton;
        this.listArea = listArea;
        this.addModal = addModal;
        this.modifyModal = modifyModal;
        this.modifyModal = modifyModal;
        this.modalAddButton = modalAddButton;
        this.modalModifyButton = modalModifyButton;
        this._initEventListener();
        this._getData();
    }

    _initEventListener() {
        this.listRefresh.click((e) => {
            this.searchArea.val("");
            this._getData();
        });
        this.modalAddButton.click((e) => {
            this._addUser(e);
        });
        this.addButton.click(() => {
            this.addModal.modal("show");
        });

        this.modalModifyButton.click((e) => {
            this._modifyUser(e);
        });

        this.listArea.on("click", "button", (e) => {
            const action = $(e.target).data("action");
            const $targetNode = $(e.target).closest("tr");
            switch (action) {
                case "modify":
                    this.modifyModal
                        .find("input[name='id']")
                        .val($targetNode.find("#user-id").text());
                    this.modifyModal
                        .find("input[name='name']")
                        .val($targetNode.find("#user-name").text());
                    this.modifyModal
                        .find("input[name='email']")
                        .val($targetNode.find("#user-email").text());
                    this.modifyModal.modal("show");
                    break;
                case "delete":
                    this._deleteUser(e);
                    break;
            }
        });

        this.searchArea.on("keyup", (e) => {
            console.log(e.target.value);
            if (e.key === "Enter") {
                e.preventDefault();
                this._getData();
                this.searchButton.focus();
            }
        });

        this.searchButton.click((e) => {
            e.preventDefault();
            this._getData();
        });
    }

    _getData() {
        debugger;
        let param = { name: this.searchArea.val() };
        PRConnect.send("/api/User/getUserByName", param, (data) => {
            this._makeList(data.result);
        });
    }

    _addUser(e) {
        const $targetNode = $(e.currentTarget).closest(".modal-content");

        const param = {
            name: $targetNode.find("input[name='name']").val(),
            email: $targetNode.find("input[name='email']").val(),
        };

        PRConnect.send("/api/User/createUser", param, (data) => {
            if (data.status === "success") {
                alert("추가 되었습니다.");
                this._getData();
            } else {
                alert("실패 하였습니다.");
            }
        });
        this.addModal.modal("hide");
        $targetNode.find("input").each(function (index, item) {
            $(item).val("");
        });
    }

    _modifyUser(e) {
        const $targetNode = $(e.currentTarget).closest(".modal-content");

        const param = {
            id: $targetNode.find("input[name='id']").val(),
            name: $targetNode.find("input[name='name']").val(),
            email: $targetNode.find("input[name='email']").val(),
        };

        PRConnect.send("/api/User/modifyUser", param, (data) => {
            if (data.status === "success") {
                alert("수정 되었습니다.");
                this._getData();
            } else {
                alert("실패 하였습니다.");
            }
        });
        this.modifyModal.modal("hide");
        $targetNode.find("input").each(function (index, item) {
            $(item).val("");
        });
    }

    _deleteUser(e) {
        const $targetNode = $(e.target).closest("tr");
        const id = $targetNode.find("#user-id").text();

        PRConnect.send("/api/User/deleteUser", { id }, (data) => {
            if (data.status === "success") {
                alert("삭제 되었습니다.");
                this._getData();
            } else {
                alert("삭제 실패 하였습니다.");
            }
        });
    }

    _makeList(list) {
        this.listArea.empty();
        this.listArea.append(this._listItemTemplate(list));
    }

    _listItemTemplate(list) {
        return list
            ?.map((user) => {
                return `<tr>
                <td id="user-id">${user.id}</td>
                <td id="user-name">${user.name}</td>
                <td id="user-email">${user.email}</td>
                <td>
                    <div class="btn-group" role="group" aria-label="Basic example">
                        <button type="button" class="btn btn-outline-primary" data-action="modify">수정</button>
                        <button type="button" class="btn btn-secondary" data-action="delete">삭제</button>
                    </div>
            </tr>`;
            })
            .join("");
    }
}
/UI/src/upload.js
import { PRFile } from "@projectroom/ui-lib";

export default class Upload {
    constructor({ uploadArea, uploadButton, fileArea }) {
        this.uploadArea = uploadArea;
        this.uploadButton = uploadButton;
        this.fileAreaja= fileArea;
        this._initEventListener();
    }

    _initEventListener() {
        this.uploadButton.click((e) => {
            this._uploadFile();
        });
    }

    _uploadFile() {
        const root = "/uploadedFiles";
        const files = this.uploadArea[0].files;
        PRFile.uploadFiles(root, files, () => {
            const list = this._listItemTemplate(files);
            this.fileArea.append(list);
        });
    }
    _listItemTemplate(files) {
        return Array.from(files)
            ?.map((file) => {
                return `<tr>
                        <td>${file.name}</td>
                        <td>${file.size}</td>
                    </tr>`;
            })
            .join("");
    }
}

만들고 하는 화면이 완성되었습니다.🥳🥳🥳🥳🥳🥳

좀 더 자세한 내용을 알고 싶으시다면, 과정 중간 중간 걸어둔 링크로 접속하시기 바랍니다. 해당 파트의 자세한 설명이 첨부되어있습니다.

Last updated