✅ "IntroDog" 은 애견인들의 의견을 교류할 수 있는 사이트입니다.
✅ 반려견을 키우면서 생기는 질문, 일상 등 사소한 것까지 다른 이들과 함께 교류할 수 있습니다!
항해99 6기 D반(심화트랙) 미니 프로젝트 보러가기
⭐ 저희 조는 8조 입니다 ⭐
user
dog
dogimg
board
reply
기능 | Method | URL | Request | Response |
---|---|---|---|---|
메인화면 페이지 로드 | GET | / | render_template('index.html') | |
로그인 페이지 로드 | GET | /login | msg | render_template('login.html', msg=errorMsg) |
회원가입 페이지 로드 | GET | /join | render_template('join.html') | |
글쓰기 페이지 로드 | GET | /board/write | Token 인증시 - render_template('board_write.html'), Token 미인증시 - {msg="로그인 정보가 존재하지 않습니다."} | |
회원가입시 선택할 강아지 목록 조회 | GET | /dog/list | jsonify({'msg': dog}) | |
ID 중복검사 | POST | /api/check_dup | {'id': id} | 중복 아닐시 - {'msg': "사용 가능한 ID 입니다"} 중복 시 - {'msg': "사용이 불가능한 ID 입니다."} |
회원가입 | POST | /api/join | {'file': file, 'email': email, 'id' :id, 'nickname':nickname, 'pw':password, 'dogCode',dog_result} | 회원가입 성공시 - {'msg': '회원가입이 완료되었습니다.'} , 실패시 - 상황에 맞는 MSG 출력 |
로그인 | POST | /api/login | {'id': id_give, 'pw': pw_give} | 로그인 성공 - {'result': 'success', 'token': token, "nickname":nickname } 로그인 실패 - {'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'} |
메인화면 강아지 이미지, 목록 조회 | GET | /getDogList | 조회 성공시 - {'dogList': dog_list, 'dogimgList': dogimg_list} | |
메인화면에서 강아지 검색어 조회 | POST | /api/search | {'give_keyword': keyword} | 조회 성공시 - {'search_dog': search_dog, 'dogimgList': dogimg_list, 'receive_keywords': receive_keywords} |
게시글 작성 | GET | /board/write | Token 인증시 - render_template('board_write.html', user_id = user_id, nickname = nickname, profileImg=user_info["profileImg"]), Token 인증 실패시 - make_response(redirect(url_for("login", errorCode=errorCode))) | |
게시글 조회 | GET | /board/select | Token 인증시 - render_template('board_list.html',result=boardList, user_id = user_id, nickname = nickname, profileImg=user_info["profileImg"] ), Token 인증 실패시 - make_response(redirect(url_for("login", errorCode=errorCode))) | |
게시글 상세 조회 | GET | /board/detail?id={id} | {'id':board_id} | Token 인증시 - render_template('board_detail.html', board=board, reply=reply_list, user_id=user_id, nickname = nickname, profileImg=user_info["profileImg"]), Token 인증 실패시 - make_response(redirect(url_for("login", errorCode=errorCode))) |
게시글 수정 화면 렌더링 | GET | /board/modify?id={id} | {'id':board_id} | render_template('board_update.html', board = board, user_id = user_id, nickname = nickname) |
게시글 저장 | POST | /board/create | {'id':id, 'userId': user_id, 'contents': contents, 'title': title, 'imgUrl': imgUrl,'createTime': now.strftime("%Y-%m-%d %H:%M:%S"),'updateTime': now.strftime("%Y-%m-%d %H:%M:%S")} | {msg="저장되었습니다."} |
게시글 수정 | PUT | /board/update | {'id':boardId, 'contents': contents, 'title': title, 'imgUrl': imgUrl,'updateTime': now.strftime("%Y-%m-%d %H:%M:%S")} | {msg="수정되었습니다."} |
게시글 삭제 | DELETE | /board/delete | {'id':id} | {msg="삭제되었습니다."} |
댓글 저장 | POST | /board/reply/create | {'boardId':board_Id ,'userId': user_id, 'contents': contents} | {msg="저장되었습니다."} |
댓글 삭제 | DELETE | /board/reply/delete | {'boardId':board_id,'seqNo' : seqNo} | {msg="삭제되었습니다."} |
FRONT-END | BACK-END | FRONT-END |
---|---|---|
레이아웃 구성 + 메인 페이지 + 검색 기능 구현 전반적인 프론트엔드 작업 |
COMMUNITY 페이지 + 글 작성/삭제 + 댓글 작성/삭제 기능 구현 전반적인 백엔드 작업 |
로그인 + 회원가입 기능 구현 전반적인 프론트엔드 작업 |
항해99 첫 주차 미니프로젝트를 진행하였습니다. 리액트-심화를 신청하였기에, D조에 배정받았으며, 3인으로 구성된 팀(8조)에서 조장을 맡게 되었습니다.
저는 이 프로젝트에서 디자인 및 프론트엔드 전반적인 작업을 html, css, javascript(jQuery)를 이용하여 구현하였고, mongoImport를 통해 mongoDB에 데이터를 넣는 작업을 하였습니다.
기본적인 뼈대는 빠른 구현을 위해 bootstrap을 사용하였습니다.
cmd에서 mongoImport를 통해 csv file을 삽입하였습니다.
1. db 에 dog.csv 넣기
mongoimport --host {mongoDB cluster명} --db {database명} --collection {collection명} --type csv --drop --columnsHaveTypes --fields "id.string(),name.auto(),desc.string()" --file {csv파일경로} --authenticationDatabase admin --ssl --username {id} --password {pw}
2. db 에 dogimg.csv 넣기
mongoimport --host {mongoDB cluster명} --db {database명} --collection {collection명} --type csv --drop --columnsHaveTypes --fields "dogId.string(),seqNo.auto(),imgUrl.string()" --file {csv파일경로} --authenticationDatabase admin --ssl --username {id} --password {pw}
강아지의 정보를 DB에 저장해야하는데 활용할 수 있는 API가 존재하지 않았고, 크롤링할 정보 받아올 만한 사이트가 없었습니다. 웹개발 플러스 강의를 수강하지 않아, 셀레니움 방식에 대한 지식이 없었기에 csv 파일을 작성한 후 cmd창에서 mongoImport를 통해 데이터를 넣어줬습니다.
몽고DB에서 data를 받아와 index에 보여주고 클릭하면 modal창이 열리는 기능을 구현하였습니다.
'use strict';
/***********************************
* Thumbnail
***********************************/
// 메인 페이지가 로드되면 thumbnail을 보여줍니다.
window.addEventListener('load', () => {
$.ajax({
type: 'GET',
url: '/getDogList',
data: {},
success: (response) => {
let rows = response['dogList'];
let rowsImg = response['dogimgList'];
for (let i = 0; i < rows.length; i++) {
// dogList의 code(id값)을 정의합니다.
let code = rows[i]['id'];
for (let j = 0; j < rowsImg.length; j++) {
// dogimgList의 imgCode(dogId 값)을 정의합니다.
let imgCode = rowsImg[j]['dogId'];
// dog의 id값과 dogimg의 dogid값이 같으면 같은 종 입니다.
if (code == imgCode) {
let name = rows[i]['name'];
let img = rowsImg[j]['imgUrl'];
let desc = rows[i]['desc'];
// thumbnail-box에 들어갈 temp_html을 정해줍니다.
// modal창에 보여주기 위해 data-bs-* 속성이 필요합니다.
let temp_html = `<button
type="button"
class="thumbnail"
data-bs-toggle="modal"
data-bs-target="#exampleModal"
data-bs-whatever="${name}"
data-bs-img="${img}"
data-bs-desc="${desc}"
>
<div class="col my-2">
<div class="card shadow-sm">
<img
src="${img}"
width="100%"
height="100%"
class="card-img-top"
title="${name}"
alt="${name}"
/>
<div class="card-body">
<p class="thunmbnail__title">${name}</p>
</div>
</div>
</div>
</button>`;
$('#thumbnail-box').append(temp_html);
break;
}
}
}
},
});
});
/***********************************
* Modal
***********************************/
// 받아온 정보를 모달창에 넣어줍니다.
window.addEventListener('load', () => {
const exampleModal = document.getElementById('exampleModal');
exampleModal.addEventListener('show.bs.modal', function (event) {
// 모달창을 열게한 버튼을 정의해줍니다.
const button = event.relatedTarget;
// data-*에서 속성을 추출해옵니다.
const recipient = button.getAttribute('data-bs-whatever');
const img = button.getAttribute('data-bs-img');
const desc = button.getAttribute('data-bs-desc');
// 모달창의 내용을 업데이트 해줍니다.
const modalTitle = exampleModal.querySelector('.modal-title');
const modalImg = document.getElementById('modal-img');
const modalBody = document.querySelector('.modal-desc');
modalTitle.textContent = recipient + ' 🐶';
modalImg.src = img;
modalBody.textContent = desc;
});
});
사전 프로젝트에서 진행했던 방식을 바탕으로 진행하였습니다.
'use strict';
const searchInput = document.querySelector('#input_kw');
const searchBtn = document.querySelector('.btn-search');
const searchForm = document.querySelector('.search__bar');
// form의 submit을 막아 줍니다.
searchForm.addEventListener('submit', (e) => {
e.preventDefault();
});
// searchBtn에 클릭이벤트를 등록합니다.
searchBtn.addEventListener('click', search);
// searchInput에서 enter(keycode:13)를 눌러도 검색이 되게 합니다.
searchInput.addEventListener('keydown', (e) => {
if (e.keyCode == 13) {
search();
}
});
function search() {
let keyword = $('#input_kw').val();
// search 함수 유효성 검사
if (keyword == '') {
alert('검색어를 입력하세요.');
return;
} else if (keyword.length < 2) {
alert('두 글자 이상 입력 해주세요.');
return;
} else if (/[^가-힣0-9a-zA-Z ]/.test(keyword) == true) {
alert('검색은 특수문자를 포함할 수 없습니다.');
return;
} else {
$.ajax({
type: 'POST',
url: '/api/search',
data: {
give_keyword: keyword,
},
success: (response) => {
let rows = response['search_dog'];
let rowsImg = response['dogimgList'];
// 검색창을 비워줍니다
let searched_wt = response['search_dog'];
searchInput.value = null;
// 썸네일 창을 비워줍니다
$('#thumbnail-box').empty();
if (searched_wt == '') {
alert('검색 결과가 없습니다.');
window.location.reload();
} else {
for (let i = 0; i < rows.length; i++) {
// dogList의 code(id값)을 정의합니다.
let code = rows[i]['id'];
for (let j = 0; j < rowsImg.length; j++) {
// dogimgList의 imgCode(dogId 값)을 정의합니다.
let imgCode = rowsImg[j]['dogId'];
// dog의 id값과 dogimg의 dogid값이 같으면 같은 종 입니다.
if (code == imgCode) {
let name = rows[i]['name'];
let img = rowsImg[j]['imgUrl'];
let desc = rows[i]['desc'];
// thumbnail-box에 들어갈 temp_html을 정해줍니다.
// modal창에 보여주기 위해 data-bs-* 속성이 필요합니다.
let temp_html = `<button
type="button"
class="thumbnail"
data-bs-toggle="modal"
data-bs-target="#exampleModal"
data-bs-whatever="${name}"
data-bs-img="${img}"
data-bs-desc="${desc}"
>
<div class="col my-2">
<div class="card shadow-sm">
<img
src="${img}"
width="100%"
height="100%"
class="card-img-top"
title="${name}"
alt="${name}"
/>
<div class="card-body">
<p class="thunmbnail__title card-text">${name}</p>
</div>
</div>
</div>
</button>`;
$('#thumbnail-box').append(temp_html);
break;
}
}
}
}
},
});
}
}
사전 프로젝트에서 진행했던 방식을 바탕으로 진행하였습니다.
jinja2를 이용한 SSR을 위해 Template 상속 및 조건문 사용
{# layout #}
{% if nickname %}
...
{% else %}
...
{% endif %}
...
<div class="contents">
{% block content %}
{% endblock %}
</div>
...
{# index.html #}
{% extends 'layout.html' %} {% block content %}
...
{% endblock %}
드디어 항해99가 본격적으로 시작했습니다. D반에서는 4명씩 일곱 조, 3명씩 3조로 구성되었고 4일간 진행을 한다고 말씀을 해주셨습니다.
1차로 조장인 것에 당황했고, 시간을 최소 5일? 정도 주는 줄 알았는데 4일이란 말에 조금 걱정이 되긴 했습니다.
팀원분들과 어색한 인사를 나누고 토의를 통해 주제를 선정하고 역할 분담을 하였습니다.
첫날에 각자 맡은 작업을 진행하였고 저녁에 멘토링 시간을 가지게 되었습니다.
이때, "사전 스터디 때 dev branch를 생성하고 개인 branch를 따로 생성하여 개인 branch에서 작업 후 dev branch에서 합치고 main에 올리는 방식이 맞느냐?"라는 질문을 드렸는데, 멘토님께서 혹시 전공자냐고 이력서 같은 곳에 한 줄 정도 넣으면 좋을 것 같다는 말씀을 해주셔서 기분이 좋았습니다.
이틀 차에 전부터 앓아왔던 컴퓨터 그래픽 카드 문제 때문에 부팅이 되지 않는 문제로 서브 컴퓨터로 구동을 하고 정신없이 오전을 보냈습니다. 다행히 임시방편으로 메인 컴퓨터를 구동할 수 있게 되어 오후 시간에는 원활하게 작업을 진행할 수 있었습니다.
개발 경력이 있는 분이 백엔드의 전반적인 담당을 해주셨는데, 덕분에 수월하게 프로젝트를 진행할 수 있었습니다. 다른 조원분도 맡은 부분을 최선을 다해서 구현하시고 서로 이야기를 나누면서 팀원들과 짧은 시간이었지만 엄청 끈끈해졌습니다.
3일 차 저녁에 'blueprint를 이용하여 기능별 py 파일 분리'하는 과정에서 서버에서 구동이 안 되는 오류가 발생했습니다. 새벽까지 문제를 해결하려 노력하신 팀원분들이 자랑스러웠습니다.
다음날에 blueprint를 이용하는 방식을 빼버리고 전반적인 디자인 및 유효성 검사를 한뒤 서둘러 18시 50분쯤에 과제 제출을 완료하게 되었습니다.
다양한 문제로 시간을 잡아먹고 서둘러 제출하였지만 완성된 결과를 보니 꽤 만족스러웠습니다.
사전스터디 경험을 살려 이번에도 전체적인 git 관리를 제가 하게 되었는데 한 팀원분께서 'git master'라 칭해주셔서 '더 열심히 해야겠다'는 다짐 또한 하게 되었습니다.
4일간의 짧은 시간이지만 즐거웠고 또 아쉽다는 생각이 들었습니다. 금요일부터 알고리즘주차가 4주 시작되는데 이후 또 같은 팀이 된다면 팀에 더 보탬이 되고싶습니다.
아쉬운 점이 있다면, 1) javascript를 이용한 기능구현을 많이 넣지 못하였고 2) 사전 스터디 때와 비슷한 작업을 한 것, 3) 멘토링 해주신 매니저님께도 많이 도움을 받았지만 리액트2 + 스프링1 인 조인데 담당 매니저님이 Spring 기술 매니저 이신게 조금 아쉬웠습니다.
이번주도 파이팅👏🏻🥲