📌 문제
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;
}
📌 실행 결과


'JavaScript > 과제테스트' 카테고리의 다른 글
[프로그래머스] [2022 Dev-Matching: 웹 프론트엔드 개발자(하반기)-2] 사원 정보 테이블 구축 문제 풀이 (0) | 2023.06.22 |
---|
댓글