본문 바로가기
PROJECT/미니 프로젝트 (24.04.16~24.04.19)

미니 프로젝트 - 팀 소개 웹페이지 개발 후기 및 코드

by HR_J 2024. 4. 21.

프로젝트 관련 링크

1일차 : 2024.04.16 - [PROJECT/미니 프로젝트 (24.04.16~24.04.19)] - 미니 프로젝트 - 팀 소개 웹페이지 개발 (1)

2일차 : 2024.04.17 - [PROJECT/미니 프로젝트 (24.04.16~24.04.19)] - 미니 프로젝트 - 팀 소개 웹페이지 개발 (2)

3일차 : 2024.04.18 - [PROJECT/미니 프로젝트 (24.04.16~24.04.19)] - 미니 프로젝트 - 팀 소개 웹페이지 개발 (3)

 


프로젝트의 진행은 아래의 순서로 진행된 것 같다.

1. 페이지 디자인 아이디어.
2. html로 구조잡기.
3. css로 꾸미기.
4. js로 동작 시키기! + firebase의 연결.

 

읽기전 알아둘 것. 보안과 관련된 부분은 전혀 고안되지 않은, 초보의 개발이다.

 

또한, 내가 만든 부분에 관련된 코드만 작성해두었다... 다른 부분들은,, 내가 작성한것이 아니라 과감하게 빼서, 최종 결과물에 있는 네비게이션(페이지 이동을 원활하게 하기 위한 기능)과 관련된 코드는 없다.


1 / 페이지 디자인 아이디어.

페이지는 팀원들 전체가 각자 디자인을 했었고, 내가 만든 디자인이 채택 되었다. 다른 사람의 디자인이 아닌 내가 만든 디자인이기 때문에, 내 편의에 맞게 수정하는 등의 작업이 진행되었다.

디자인을 하고 나서, 필요한 기능들에 대해 천천히 생각해보았다.

팀원 리스트에 적힌 팀원의 이름 버튼을 누를시, 해당 팀원의 정보가 나와야 한다. 버튼에 적힌 이름과 DB에 기입된 이름이 같을 경우 데이터 받아오는것이다.

또, 수정버튼을 누를 경우 수정 페이지로 이동되어야한다. ( 이부분은 다른 팀원이 맡았다! 천재 팀원!)

이것이 가장 큰 기능이라, 개인적으로 개발에 큰 어려움이 없을 것이라 예상했다. CSS 다루는게 문제라면 문제겠지만...

 

2 / html로 구조잡기.

2-1 / <head> 태그 부분

코드가 지나치게 길어지는 부분이 마음에 들지 않아 파일을 3개로 분리했다. -->  html, css, js파일.

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    
    <!--jsquery라이브러리를 불러오는 코드, 그리고 member.js파일을 연결시키는 코드.
    이곳에 라이브러리를 불러왔기 때문에, js파일에서 편하게 jsquery를 사용했다!-->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="member.js" type="module"></script>
    
    <!--css 파일을 연결시키는 코드.-->
    <link rel="stylesheet" href="member.css" />

	<!--필수는 아니지만,,, 파비콘과 타이틀을 지정해주고 싶어서 작성한 코드.
    icon타입으로 넣을 경우 type을 x-icon으로 지정해주어야 한다
    참고로 href로 연결한 이미지는 엄청엄청 귀여운 강아지 파비콘이다.-->
    <link
        href="data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAADZKgAAAAAAACV/wAnJysA+vr6AAFHeABmZnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAgICAQECAgICAQEBAQEBAgQEBAICBAQEAgIBAQEBAQIEBAQEBAQEBAQCAQEBAQUEBAQEAwMEBAQEBAUFAQUFBAQEBAYGBAQEBAQFBQUFAAQCAwQEBAQEAwICAAUFAAACAgMEBAQEAgMCAgAABQAAAgICBAQEBAICAgAAAAAAAAACAgQEBAQCAgIAAAAAAAAAAgIEBAQEAgICAAAAAAAAAAICAgQEBAICAAAAAAABAAAAAgIEBAICAgAAAAABAQEAAAACBAQCAgAAAAABAQEBAQAAAQEBAQEAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAeMPAADABwAAwAcAAIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAEAAMADAADnxwAA//8AAP//AAA="
        rel="icon" type="image/x-icon">
    <title>팀원 소개</title>

</head>

 

2-2 / <body> 태그 부분

<!--body에 있는 내용들 드래그, 우클릭 등 불가능하게 처리!-->
<body oncontextmenu="return false" ondragstart="return false" onselectstart="return false">

    <div class="view">
    
    	<!--팀원 리스트 구역.-->
        <div class="teamlist">
            <img class="dogImg" id="dog" src="https://ifh.cc/g/0wNK7k.png">
            <div class="list"></div>
        </div>

		<!-- 사원증 영역-->
        <div class="main">
        
        	<!--카드 앞면 영역-->
            <div class="front-card">
                <div class="mem-img">
                    <img id="photo" src="https://ifh.cc/g/DF0RCK.png">
                </div>
                <div class="mem-name" id="name">팀원 이름</div>
                <div class="mem-mbti" id="mbti">MBTI</div>
                <div class="social-ic">
                    <a target='_blank' class="github" id="github"><img class="github-img"
                            src="https://ifh.cc/g/WzANcb.png" /></a>
                    <a target='_blank' class="blog" id="blog"><img class="blog-img"
                            src="https://ifh.cc/g/rjCyQk.png" /></a>
                    <a target='_blank' class="email" id="email"><img class="email-img"
                            src="https://ifh.cc/g/WFnftW.png" /></a>
                </div>
            </div>
			
            <!-- 카드 뒷면 영역 -->
            <div class="back-card">
                <div class="area">
                    <div class="mem-self-intro">자기소개 : <span id="intro"></span></div>
                    <div class="mem-hobby">취미 : <span id="hobby"></span></div>
                    <div class="mem-sw">장단점 : <span id="goodbad"></span></div>
                    <div class="mem-stack">기술 스택 : <span id="stack"></span></div>
                    <div class="mem-coop-style">협업스타일 : <span id="coop"></span></div>
                    <div class="mem-note">한마디 : <span id="oneword"></span></div>
                </div>
            </div>

        </div>
        
        <!--수정하기 버튼 영역. 이미지에 버튼기능을 추가했다. 클릭시 연결시켜둔 페이지로 이동한다.-->
        <img class="btnimg" src="<!--원하는 이미지 소스-->"
            onclick="location.href = '<!--원하는 경로-->'" />
    </div>
</body>

id, class 명으로 지정해 둔 것들은 해당 구역(영역)에 대한 처리를 css와 js에서 하기 위함.

3 / css로 꾸미기.

3-0 / 기본 설정.

/* 폰트 불러오기 */
@import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic&display=swap');

/* 전체 폰트 지정 */
* {
    font-family: "Nanum Gothic", sans-serif;
    font-weight: bold;
    font-style: normal;

    color: #31344b;
    font-size: 17px;
}

/* body 전체 영역에 대한 정보
	사실 다른 페이지와 전부 동일해야하는 영역이라 내가 작성해둔 코드가 아니다. */
body {
    background-image: url("/* 원하는 배경 이미지 소스 */");
    background-size: cover;

    background-attachment: fixed; /*배경이미지 고정*/
    margin: 200px 0px 0px 0px;
    font: 1em/1.4 Sans-serif;
    color: #fff;
}

 

3-1 / view 영역

.view {

    display: flex;
    justify-content: center;
    align-items: center;
    margin: 50px auto 50px auto;

    width: 1000px;
    height: 550px;

    background: rgba(255, 255, 255, 0.6);
    border-radius: 10px;

}

 

3-2 / teamlist 영역

/* 팀 리스트 영역 설정 */
.teamlist {
	/* 크기 설정 */
    width: 200px;
    height: 550px;

    background-color: #ffc851;
    padding: 20px;
    border: none;
    border-radius: 8px;
}

/* 팀 리스트 제일 상단에 위치하는 강아지 아이콘 설정 */
.teamlist .dogImg {
    /* 이미지 크기 지정 */
    height: 50px;
    width: 50px;

	/* 세로 정렬 */
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin: 20px auto;
}

/* 버튼이 많아질 경우를 위해 스크롤 기능 추가 */
.teamlist .list {
    height: 420px;
    
    /*정확히 이 부분*/
    overflow: auto;
    
    background:#ffebbe;
    padding:10px;
    border-radius: 8px;
}

.teamlist .list::-webkit-scrollbar {
    padding: 2px;
    border-radius: 8px;
    width: 8px;
}

.teamlist .list::-webkit-scrollbar-thumb {
    height: 30%;
    background: #ffd77f;
    border-radius: 25px;
}

.teamlist .list::-webkit-scrollbar-track {
    background:#ffebbe;
    padding: 2px;
    border-radius: 8px;
}

.list>button {
    background: white;
}

/* 팀원 이름이 적혀있는 버튼 디자인*/
.teambtn {
    height: 50px;
    width: 100%;

    background-color: whitesmoke;

    border: none;
    border-radius: 8px;

    display: flex;
    flex-direction: column;
    justify-content: center;

    padding: 20px;
    margin-top: 5px;
}

 

3-3 / main 영역

사원증 카드가 들어있는 영역이다. teamlist영역 바로 옆에 붙어 있다.

.main {

    display: flex;
    /* 덕분에 사원증의 앞면과 뒷면의 배치가 가로로 나열되게 되었다. */
    flex-direction: row;
    align-items: center;
    justify-content: center;

	/* 카드들이 배치될 공간 자체에 크기를 지정해주어 보다 배치를 예쁘게 해보았다..ㅎ */
    width: 700px;
    height: 550px;
    margin: 50px;

}

/* 사원증 안의 폰트 줄간격 처리. */
.front-card .mem-name,
.front-card .mem-mbti,
.back-card .mem-stack,
.back-card .mem-hobby,
.back-card .mem-self-intro,
.back-card .mem-sw,
.back-card .mem-coop-style,
.back-card .mem-note {

    font-style: normal;
    margin: 10px 0 5px 0;
}

 

3-4 / front-card 영역

.front-card {
    background: #ffffff;
    /* 시계 반대방향으로 10도 정도 회전을 넣었다. */
    transform: rotate(-10deg);
    /* depth를 주었다. back-card의 값은 0으로 주어 무조건 back-card의 앞에 배치 된다. */
    z-index: 1;

	/* 사원증 테두리 및 테두리 그림자 설정 */
    border: solid 1px #808080;
    box-shadow: 3px 3px 5px rgb(39, 39, 39);

	/* 사원증 앞면의 형태 지정 */
    position: relative;
    width: 300px;
    height: 450px;
    padding: 10px;
    border-radius: 8px;
	
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
}

/* 카드 앞면 하단에 배치되는 아이콘들의 크기 지정 */
.front-card .social-ic .github-img,
.front-card .social-ic .blog-img,
.front-card .social-ic .email-img {
    width: 30px;
    height: 30px;
}

/* 아이콘들 사이의 간격 지정 */
.front-card .social-ic {
    margin: 15px 0 25px 0;
}

/* 프로필 사진이 위치하는 공간 */
.mem-img>img {
	/* 사진 크기 지정*/
    width: 200px;
    height: 200px;
    
    /* 마진 지정 */
    margin-top: 30px;
    margin-bottom: 10px;
    border-radius: 10px;
    margin-bottom: 20px;

	/* 사진의 왜곡(늘어남, 눌림 등) 없이 공간을 채움. */
    object-fit: cover;
}

 

3-5 / back-card 영역

/* front-card와 코드가 거의 똑같으나, z-index가 다름 */
.back-card {
    background: #ffdb8d;
    z-index: 0;

    border: solid 1px #808080;
    box-shadow: 3px 3px 5px rgb(39, 39, 39);

    position: relative;
    width: 300px;
    height: 450px;
    padding:10px;
    border-radius: 8px;

    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    
}

/* 스크롤될 영역 지정. */
.back-card .area{
    height: 420px;
    width:250px;
    
    /* 카드 안에 들어가는 요소들이 범위를 넘어갈 경우 스크롤이 되도록 처리. */
    overflow: auto;
    
    border-radius: 8px;
    padding:10px;
    
    display: flex;
    justify-content: center;
    flex-direction: column;
}

/* 아래의 3개의 블럭은 스크롤 기능을 추가하기 위한 코드 */
.back-card .area::-webkit-scrollbar {
    padding: 2px;
    border-radius: 8px;
    width: 8px;
}

.back-card .area::-webkit-scrollbar-thumb {
    height: 30%;
    background: #ffbf34;
    border-radius: 25px;
}

.back-card .area::-webkit-scrollbar-track {
    background:#ffebbe;
    padding: 2px;
    border-radius: 8px;
}

/* 텍스트 왼쪽 정렬 */
.mem-self-intro,
.mem-hobby,
.mem-sw,
.mem-stack,
.mem-coop-style,
.mem-note {
    text-align: left;
}

 

3-6 / 수정하기 버튼 영역

.btnimg{

	/* 크기 지정 */
    width:50px;
    height:50px;
    
    /* flex 속성을 부여한 후, 순서대로, 가로축, 세로축 중심 center에 배치*/
    display: flex;
    justify-content: center;
    align-items: center;
    
    /* 위치 고정 - 화면 우측 하단으로 부터 30px씩 떨어진 곳. */
    position:absolute;
    bottom:30px;
    right:30px;
    
    /* 배경 색과 곡선도(모서리 부분) 지정 */
    background-color: #ffffff;
    border-radius: 10px;
}

 

4 / js로 동작 시키기! + firebase의 연결.

4-0 / DB 데이터

내가 작업하는 페이지에 꼭 필요한 데이터들

 

팀원들과의 회의 끝에 나온 members 테이블.

ID는 유저 식별을 위한 docs 이름이다. 변경이 불가능하다.

아래의 모든 정보들은 내가 디자인한 사원증 형태의 팀원 소개 페이지에 들어가게 된다.

 

4-1/ 파이어베이스 연결.

정확하게는 파이어 베이스에서 제공하는 서비스 중 파이어스토어를 사용한다. ( NoSQL)

// Firebase SDK 라이브러리를 가져온다.
// 사용할 라이브러리 불러오기(필요에 따라 Firebase Docs를 확인하여 추가할 것!)
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-app.js";
import { getFirestore, collection, getDocs} from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";


// Firebase 구성 정보 설정
const firebaseConfig = {
	//파이어베이스 > 프로젝트 개요(설정)> 프로젝트 설정
    // > 일반 > 내 앱 > SDK 설정 및 구성 > 구성 > firebaseConfig 복사해 넣기
};


// Firebase 인스턴스 초기화
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

 

4-2 / DB에서 팀원 이름에 대한 정보를 받아와 팀원 리스트 만들기.

/* firebase의 collection 이름이 members다. */
const myCollection = collection(db, "members");

/* 해당 collection의 데이터를 불러온다. <getDocs> */
const querySnapshot = await getDocs(myCollection)

/* collection안의 전체 docs를 하나씩 검토해가며 이름을 불러온다. */
querySnapshot.forEach(doc => {
    const teamname = doc.data().name
    
    /* id = list 인 영역*/
    var teamList = $('.list')

	/* 팀원인지 아닌지를 판별해서, 팀원인 경우에만 버튼이 동적으로 생성되어 나타나도록 하는 코드 */
    if(doc.data().isTeam==true)
    {
        var button = $('<button>').attr('type', 'button').addClass('teambtn').attr('val', teamname).text(teamname);
        /* 해당 영역에 위에서 만든 버튼을 붙인다.*/
        teamList.append(button);
    }
})

 

4-3 / 데이터 불러오기.

/* 동적으로 생성해둔 버튼들 */
const buttons = document.querySelectorAll('.teambtn')

/* DB의 필드에 아무런 값이 없을 경우에 보여줄 초기 값을 셋팅한다. */
const defaults = {
    mbti: 'null',
    techStack: 'null',
    intro: 'null',
    hobby: 'null',
    goodBad: 'null',
    coopStyle: 'null',
    oneWord: 'null',
    github: null,
    blog: null,
    email: null,
    photo: 'https://ifh.cc/g/DF0RCK.png'
}


/* 각각의 팀원 버튼에 대한 처리 */
buttons.forEach(button => {

	/* 버튼을 눌렀을 경우, 데이터를 실질적으로 넣어주는 코드 */
    button.addEventListener('click', async () => {
        const value = button.textContent

        try {
        	/* collection이 비어있는지 확인한다! -> 비어있을때는 작동이 안되어야하기 때문 */
            if (!querySnapshot.empty) {
                querySnapshot.forEach(doc => {
                    const data = doc.data()
                    
                    /* 팀원 버튼에 기입된 이름이 data.name과 같은 경우*/
                    if (data.name == value) {
                    
                    /* 비어있는 곳이 있는 경우는 default의 값이 입력 되도록
                    데이터가 입력되어 있는 경우에는 기록되어있는 데이터가 입력되도록한다.*/
                        const filledData = {
                            name: data.name,
                            mbti: data.mbti || defaults.mbti,
                            techStack: data.techStack || defaults.techStack,
                            intro: data.intro || defaults.intro,
                            hobby: data.hobby || defaults.hobby,
                            goodBad: data.goodBad || defaults.goodBad,
                            coopStyle: data.coopStyle || defaults.coopStyle,
                            oneWord: data.oneWord || defaults.oneWord,
                            github: data.github || null,
                            blog: data.blog || null,
                            email: data.email || null,
                            photo: data.photo || defaults.photo
                        }
                        
                        /* 데이터를 화면에 보여주기 위한 코드들. 해당 영역의 text를 데이터로 지정한다. */
                        $('#name').text(filledData.name)
                        $('#mbti').text(filledData.mbti)
                        $('#intro').text(filledData.intro)
                        $('#hobby').text(filledData.hobby)
                        $('#goodbad').text(filledData.goodBad)
                        $('#stack').text(filledData.techStack)
                        $('#coop').text(filledData.coopStyle)
                        $('#oneword').text(filledData.oneWord)


						/* 깃허브 주소 연결 */
                        if (filledData.github) {
                        /* 데이터 앞에 https://가 있는지 없는지를 체크하고 없을경우 붙여서
                        해당 링크로 이동하도록 처리했다.*/
                            const githubUrlPattern = /^(https?:\/\/)/
                            const isExist = githubUrlPattern.test(filledData.github)

                            if (isExist) {
                                console.log("yes protocol")
                            } else {
                                console.log("no protocol")
                                if(filledData.github.length==1)
                                {
                                    filledData.github=null
                                }
                                else{
                                    filledData.github = "https://" + filledData.github
                                }
                            }
                        } else {
                            console.log("no data")
                        }

						/* 블로그 주소 */
                        if (filledData.blog) {
                        /* 데이터 앞에 https://가 있는지 없는지를 체크하고 없을경우 붙여서
                        해당 링크로 이동하도록 처리했다.*/
                            const blogUrlPattern = /^(https?:\/\/)/
                            const isExist = blogUrlPattern.test(filledData.blog)

                            if (isExist) {
                                console.log("yes protocol")
                            } else {
                                console.log("no protocol")
                                if(filledData.blog.length==1)
                                {
                                    filledData.blog=null
                                }
                                else{
                                    filledData.blog = "https://" + filledData.blog
                                }
                            }
                        } else {
                            console.log("no data")
                        }

                        $('#github').attr('href', filledData.github)
                        $('#blog').attr('href', filledData.blog)


						/* 메일 */
                        if (filledData.email) {
                            
                            /* 메일이 올바른 형식인지 체크한다. */
                            const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
                            const isValidEmail = emailPattern.test(filledData.email)

							/* 올바른 형식일 경우에만 mailto로 바로 메일을 보낼 수 있게 연결한다. */
                            if (isValidEmail) {
                                const emailLink = "mailto:" + filledData.email
                                $('#email').attr('href', emailLink)
                            } else {
                                console.log("not acceptable data")
                            }
                        } else {
                            console.log("no data")
                            filledData.email=null
                        }

                        $('#photo').attr('src', filledData.photo)

                    }

                })
            } else {
                console.log('data empty')
            }
        } catch (error) {
            console.error('get error', error)
        }
    })
})

완벽한 코드가 아니다... 정리해야할 부분도 많이 보이거니와, 좀 더럽다... 그렇지만 작동은 한다!

 


전체 감상문

내가 담당하는 부분들이 난이도가 있는 편은 아니었기 때문에, 무난하게 스스로 모든 문제를 해결할 수 있었다. 대신 해당 언어들의 사용이 미숙하다보니 코드가 상당히 더럽다고 느껴진다. 당장 수정할 여유는 없어 아마 리팩토링 하게된다면, 기존에 작성해둔 코드를 수정해서 다시 업로드하게 될 것같다.

아무튼 3일간의 짧다면 짧고, 길다면 긴 프로젝트가 끝이났다. 프로젝트의 개인 기록을 이런식으로 해보는 것은 또 처음이라, 많이 부족한 부분도 보였다. 하지만, 이 모든것은 반복이 해결해줄것이다.

열심히 하자, 몰라도 일단 해보자!