본문 바로가기
JavaScript/과제테스트

[프로그래머스] [2022 Dev-Matching: 웹 프론트엔드 개발자(하반기)] 인사 정보 SPA 리뉴얼 문제 풀이

by 김춘삼씨의 고양이 2023. 6. 22.

📌 문제

https://school.programmers.co.kr/skill_check_assignments/331

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

📌 풀이

소스코드 디렉터리 구조

 

index.html

<!DOCTYPE html>
<html lang="ko">
<link rel="stylesheet" href="/web/style.css">

<head>
    <meta charset="UTF-8">
    <title>2022 Dev-Matching: 웹 프론트엔드 개발자(하반기)-1</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" rel="stylesheet">
</head>

<body>
    <div class="app"></div>
    <script type="module" src="/web/src/index.js"></script>
</body>

</html>

 

index.js

import App from "./App.js";

new App(document.querySelector(".app")).render();

 

App.js

import Header from "./components/Header.js";
import { setPersonalInfo } from "./components/Storage.js";
import HomePage from "./page/HomePage.js";
import SignupPage from "./page/SignupPage.js";

class App {
    constructor($app) {
        this.$app = $app;
    }

    async render() {
        await setPersonalInfo();

        const header = new Header(this.$app);
        header.render();

        const main = document.createElement("main");
        main.setAttribute("id", "page_content");
        this.$app.appendChild(main);

        const homePage = new HomePage(main);
        const signupPage = new SignupPage(main);

        homePage.render();

        document.addEventListener("urlchange", (event) => {
            let pathName = event.detail.href;
            main.innerHTML = '';
            
            switch(pathName) {
                case "/web/":
                    homePage.render();
                    break;
                case "/web/signup":
                    signupPage.render();
                    break;
                default: break;
            }
        })
    }
}

export default App;

 

components/Card.js

export const createCardDiv = (idx) => {
    const card_div = document.createElement("div");
    card_div.setAttribute("idx", idx);
    card_div.setAttribute("class", "card");

    let cardStatusStorage = JSON.parse(localStorage.getItem("cardStatus"));

    if(!cardStatusStorage[idx]) {
        const cardStatus = {
            idx: idx,
            status: "card",
        }
        cardStatusStorage.push(cardStatus);
        localStorage.setItem("cardStatus", JSON.stringify(cardStatusStorage));
    } else {
        card_div.setAttribute("class", cardStatusStorage[idx].status);
    }

    card_div.addEventListener("click", (e) => {
        card_div.classList.toggle("is-flipped");

        let cardStatusStorage = JSON.parse(localStorage.getItem("cardStatus"));
        cardStatusStorage[idx].status = card_div.classList.value;
        localStorage.setItem("cardStatus", JSON.stringify(cardStatusStorage));
    });

    return card_div;
}

export const setCardContent = (side, data) => {
    const card_content = document.createElement("div");
    card_content.setAttribute("class", `card_plane card_plane--${side}`);
    card_content.appendChild(document.createTextNode(data));

    return card_content;
}

 

components/CardView.js

import { createCardDiv, setCardContent } from "./Card.js";
import { setCardStatus, setPersonalInfo } from "./Storage.js";

class CardView {
    constructor($main) {
        this.$main = $main;
    };

    infinateScroll = (card_container, personalInfo) => {
        let target = card_container.lastChild;

        const io = new IntersectionObserver((entry) => {
            if(entry[0].isIntersecting) {
                io.unobserve(target);

                const start = parseInt(target.getAttribute("idx")) + 1;
                const end = start + personalInfo.length;

                for(let i=start; i<end; i++) {
                    const card_div = createCardDiv(i);
                    const card_nickname = setCardContent("front", personalInfo[i-start].nickname);
                    const card_mbti = setCardContent("back", personalInfo[i-start].mbti);
        
                    card_div.appendChild(card_nickname);
                    card_div.appendChild(card_mbti);
                    card_container.appendChild(card_div);
                }

                target = card_container.lastChild;

                io.observe(target);
            }
        }, {
            threshold: 0.7
        });
        
        io.observe(target);
    }

    async redner() {
        const card_container = document.createElement("div");
        card_container.setAttribute("id", "cards_container");

        await setPersonalInfo();
        const personalInfo = JSON.parse(localStorage.getItem("personalInfo"));

        setCardStatus();

        for(let i in personalInfo) {
            const card_div = createCardDiv(i);
            const card_nickname = setCardContent("front", personalInfo[i].nickname);
            const card_mbti = setCardContent("back", personalInfo[i].mbti);

            card_div.appendChild(card_nickname);
            card_div.appendChild(card_mbti);
            card_container.appendChild(card_div);
        }

        this.$main.appendChild(card_container);

        this.infinateScroll(card_container, personalInfo);
    };
}

export default CardView;

 

components/ContentTitle.js

class ContentTitle {
    constructor($main, $title) {
        this.$main = $main;
        this.$title = $title;
    }

    render() {
        const div = document.createElement("div");
        div.setAttribute("class", "content_title");

        const h1 = document.createElement("h1");

        h1.appendChild(document.createTextNode(this.$title));
        div.appendChild(h1);
        this.$main.appendChild(div);
    }
}

export default ContentTitle;

 

components/Form.js

const setLabel = (labelId, labelName, isRequired) => {
    const label = document.createElement("label");
    label.setAttribute("for", labelId);
    label.appendChild(document.createTextNode(labelName));

    if(isRequired) {
        const label_mark =  document.createElement("span");
        label_mark.setAttribute("class", "mark");
        label_mark.appendChild(document.createTextNode("(필수*)"));
        label.appendChild(label_mark);
    }

    return label;
}

export const textField = (inputId, inputType, inputName, isRequired) => {
    const span = document.createElement("span");
    span.setAttribute("class", "form_elem");

    const input = document.createElement("input");
    input.setAttribute("id", inputId);
    input.setAttribute("type", inputType);
    input.setAttribute("placeholder", inputName);

    if(isRequired) {
        input.setAttribute("required", isRequired);
    }

    if(inputId === "name") {
        input.setAttribute("pattern", "^([가-힣]){2,4}$");
        input.setAttribute("title", "2~4 글자의 한글만 입력이 가능합니다.");
    } else if (inputId === "email") {
        input.setAttribute("pattern", "^[a-zA-Z0-9]+@grepp.co$");
        input.setAttribute("title", "이메일 ID는 영문(대소문자 구분 없음)과 숫자만 입력이 가능하며, @grepp.co 형식의 이메일만 입력이 가능합니다.");
    } else if (inputId === "nickname") {
        input.setAttribute("pattern", "^[a-zA-Z]{3,10}$");
        input.setAttribute("title", "대소문자 구분 없이 3~10 글자의 영문만 입력이 가능합니다.");
    }

    span.appendChild(setLabel(inputId, inputName, isRequired));
    span.appendChild(input);

    return span;
}

export const selectField = (data, selectId, selectName, titleText, isRequired) => {
    const span = document.createElement("span");
    span.setAttribute("class", "form_elem");

    const select = document.createElement("select");
    select.setAttribute("id", selectId);
    select.setAttribute("name", selectId);

    if(isRequired) {
        select.setAttribute("required", isRequired);
    }

    const option_title = document.createElement("option");
    option_title.setAttribute("value", "");
    option_title.appendChild(document.createTextNode(titleText));
    select.appendChild(option_title);

    for(let i in data) {
        const option = document.createElement("option");
        option.setAttribute("value", data[i].val);
        option.appendChild(document.createTextNode(data[i].name));
        select.appendChild(option);
    }

    span.appendChild(setLabel(selectId, selectName, isRequired));
    span.appendChild(select);

    return span;
}

export const submitBtn = () => {
    const span = document.createElement("span");
    span.setAttribute("class", "form_elem");

    const button = document.createElement("button");
    button.setAttribute("type", "submit");
    button.appendChild(document.createTextNode("등록"));

    span.appendChild(button);

    return span;
}

 

components/Header.js

class Header {
    constructor($app) {
        this.$app = $app;
    }

    createMenuElem = (divClass, spanClass, spanId, menuName, url) => {
        const div = document.createElement("div");
        div.setAttribute("class", divClass);

        const span = document.createElement("span");
        span.setAttribute("class", spanClass);
        span.setAttribute("id", spanId);

        span.appendChild(document.createTextNode(menuName));

        span.addEventListener("click", () => {
            history.pushState("", "", url);
            const urlChange = new CustomEvent("urlchange", {
                detail: { href: url }
            });
            document.dispatchEvent(urlChange);
        });

        div.appendChild(span);

        return div;
    }
    
    render() {
        const header = document.createElement("header");

        const home_menu = this.createMenuElem("header header_left", "menu_name", "menu_home", "HOME", "/web/");
        const signup_menu = this.createMenuElem("header header_right", "menu_name", "menu_signup", "SIGNUP", "/web/signup");
        
        header.appendChild(home_menu);
        header.appendChild(signup_menu);

        this.$app.appendChild(header);
    }
}

export default Header;

 

components/SignupView.js

 

import { textField, selectField, submitBtn,  } from "./Form.js";
import { role_data, mbti_data } from "../data/FormData.js";

class SignupView {
    constructor($main) {
        this.$main = $main;
    }

    render() {
        const form_container = document.createElement("div");
        form_container.setAttribute("id", "form_container");

        const form = document.createElement("form");
        form.setAttribute("id", "grepp_form");

        form.appendChild(textField("name", "text", "이름", true));
        form.appendChild(textField("email", "email", "이메일", true));
        form.appendChild(textField("nickname", "text", "닉네임", true));

        form.appendChild(selectField(role_data, "role", "직군", "직군을 선택해주세요", true));
        form.appendChild(selectField(mbti_data, "mbti", "MBTI", "MBTI를 선택해주세요", false));

        form.appendChild(submitBtn());

        form.addEventListener("submit", (e) => {
            e.preventDefault();
    
            let nameVal = e.target.name.value;
            let emailVal = e.target.email.value;
            let nicknameVal = e.target.nickname.value;
            let roleVal = e.target.role.options[e.target.role.selectedIndex].text;
            let mbtiVal = e.target.mbti.options[e.target.mbti.selectedIndex].text;

            let personalInfo = JSON.parse(localStorage.getItem("personalInfo"));

            const form_info = {
                "idx": Number(personalInfo.length),
                "name": nameVal,
                "email": emailVal,
                "nickname": nicknameVal,
                "role": roleVal,
                "mbti": mbtiVal
            };

            if(personalInfo.find(el => el.name=== nameVal) || personalInfo.find(el => el.nickname === nicknameVal)) {
                alert("이메일 혹은 닉네임이 이미 등록되어 있습니다.");
                return;
            } else {
                personalInfo.push(form_info);
                localStorage.setItem("personalInfo", JSON.stringify(personalInfo));

                history.pushState("", "", "/web/signup");
                const urlChange = new CustomEvent("urlchange", {
                    detail: { href: "/web/signup" }
                });
                document.dispatchEvent(urlChange);

                alert("성공적으로 등록되었습니다.");
            }
        });

        form_container.appendChild(form);
        this.$main.appendChild(form_container);
    }
}

export default SignupView;

 

components/Storage.js

export const setPersonalInfo = async() => {
    const response = await fetch("/web/src/data/new_data.json");
    const data = await response.json();

    if(!localStorage.getItem("personalInfo")) {
        let personalInfoArr = data.map((info, index) => {
            return {
                idx: index,
                name: info.name,
                email: info.email,
                nickname: info.nickname,
                role: info.role,
                mbti: info.mbti,
            }
        });

        localStorage.setItem("personalInfo", JSON.stringify(personalInfoArr));
    }
};

export const setCardStatus = () => {
    if(!localStorage.getItem("cardStatus")) {
        localStorage.setItem("cardStatus", JSON.stringify([]));
    }
}

 

 

page/HomePage.js

import CardView from "../components/CardView.js";
import ContentTitle from "../components/ContentTitle.js";

class HomePage {
    constructor($main) {
        this.$main = $main;
    }

    render() {
        new ContentTitle(this.$main, "Great PeoPle").render();
        
        const card_view = new CardView(this.$main);
        card_view.redner();
    }
}

export default HomePage;

 

page/SignupPage.js

import ContentTitle from "../components/ContentTitle.js";
import SignupView from "../components/SignupView.js";

class SignupPage {
    constructor($main) {
        this.$main = $main;
    }

    render() {
        new ContentTitle(this.$main, "Sign Up, GreatPeoPle!").render();

        const signup_view = new SignupView(this.$main);
        signup_view.render();
    }
}

export default SignupPage;

 

data/FormData.js

export const role_data = [
    {
        "val": "backend",
        "name": "백엔드"
    },
    {
        "val": "frontend",
        "name": "프론트엔드"
    },
    {
        "val": "fullstack",
        "name": "풀스택"
    }
];

export const mbti_data = [
    {
        "val": "enfj",
        "name": "ENFJ"
    },
    {
        "val": "entj",
        "name": "ENTJ"
    },
    {
        "val": "enfp",
        "name": "ENFP"
    },
    {
        "val": "entp",
        "name": "ENTP"
    },
    {
        "val": "esfj",
        "name": "ESFJ"
    },
    {
        "val": "estj",
        "name": "ESTJ"
    },
    {
        "val": "esfp",
        "name": "ESFP"
    },
    {
        "val": "estp",
        "name": "ESTP"
    },
    {
        "val": "infj",
        "name": "INFJ"
    },
    {
        "val": "intj",
        "name": "INTJ"
    },
    {
        "val": "infp",
        "name": "INFP"
    },
    {
        "val": "intp",
        "name": "INTP"
    },
    {
        "val": "isfj",
        "name": "ISFJ"
    },
    {
        "val": "istj",
        "name": "ISTJ"
    },
    {
        "val": "isfp",
        "name": "ISFP"
    },
    {
        "val": "istp",
        "name": "ISTP"
    }
]

 

style.css

body {
    margin: 0;
    padding: 0;
}

/* GNB */
header {
    display: flex;
    height: 60px;
    align-items: center;
    font-size: 1.5em;
    font-family: 'Bebas Neue', cursive;
    background: #29323C;
    background: linear-gradient(198deg, rgba(2,0,36,1) 0%, rgba(99,116,136,1) 0%, rgba(41,50,60,1) 100%);
    color: white;
}

.header {
    padding: 0px 20px;
}

.header_left {
    position: absolute;
    left: 15px;
}

.header_right {
    position: absolute;
    right: 15px;
}

.menu_name:hover {
    cursor: pointer;
}

#page_content {
    padding: 20px;
}

.content_title {
    text-align: center;
}

/* 인사 정보 페이지 */
#cards_container {
    width: 80%;
    margin: 10px auto;
    padding: 10px 10px;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    align-content: space-around;
}

.card {
    margin: 20px 10px;
    width: 200px;       
    height: 260px;
    border-radius : 25px;
    transform-style: preserve-3d;
    transform-origin: center right;
    transition: transform 1s;
    box-shadow: 5px 5px 10px rgba(0, 0, 0, .2);
    cursor: pointer;
}

.card.is-flipped {
    transform: translateX(-100%) rotateY(-180deg);
}

.card_plane {
    width: 100%;
    height: 100%;
    border-radius : 25px;
    text-align: center;
    font-weight: 700;   
    font-size: 40px;    
    color: white;     
    backface-visibility: hidden;
    position: absolute;
    display: flex;
    justify-content: center;
    align-items: center;
}

.card_plane--front {
    background-color: #0D0D0D;    
}

.card_plane--back {
    background-color: #F2F2F2;
    color: #3098F2;;
    transform: rotateY(180deg);
}

/* 인사 정보 등록 페이지 */
#form_container {
    align-items: center;
    text-align: center;
    margin: 30px 0px;
}

.form_elem {
    display: block;
    width: 250px;
    margin: 0.75em auto;
}

.form_elem label {
    display: flex;
    padding: 3px 5px;
    font-size: 0.7em;
    color: #5A5C66;
}

.form_elem input {
    width: 100%;
    padding: 1em;
    border: solid 1.5px #9E9E9E;
    border-radius: 1rem;
    box-sizing: border-box;
    background: none;
    font-size: 1.0em;
    color: #5A5C66;
}

.form_elem select {
    width: 100%;
    padding: 1em;
    border: solid 1.5px #9E9E9E;
    border-radius: 1rem;
    box-sizing: border-box;
    background: none;
    font-size: 1.0em;
    color: #5A5C66;
}

.form_elem button {
    width: 100%;
    padding: 1em;
    border: 0;
    border-radius: 1rem;
    box-sizing: border-box;
    color: white;
    font-size: 1.0em;
    background-color: #29323C;
}
.form_elem button:hover {
    cursor: pointer;
    background-color: #637488;
}

span.mark {
    color: red;
}

 

📌 실행 결과

 

댓글