현대인들의 건강에 대한 관심이 올라가면서 운동에 대한 수요도 높아지고 있습니다. 혼자 운동을 하는 경우도 많지만, 운동하는 사람들이 늘어남에 따라서 같은 운동을 함께하고자 소모임이 많아진 것을 볼 수 있습니다. 하지만 운동 모임의 수요에 비해 모임 약속을 정하는 체제가 불편한 것을 알 수 있었습니다.
기존에는 카카오 톡방으로 모임의 출석 인원과 시간, 장소를 정하고 있지만, 가독성이 좋지 못할 뿐만 아니라, 모임 시간과 장소를 변경하기에 어려움이 있고, 더 많은 모임을 수용하기에 불편합니다.
그렇기에 운동 모임을 보다 쉽고 가독성 있게 확인할 수 있고, 시간별, 장소별로 한 눈에 볼 수 있도록 운동 체크 앱을 기획 및 설계하고자 합니다. 또한, 회원들의 출석일과 출석 시간을 저장하여 회원별로 출석률을 확인할 수 있고, 개인 페이지에 운동한 날짜, 장소를 기록하여 개인별로 운동 확인을 도울 수 있습니다.
<그림1> 카카오 톡방 모임 예시
페어플레이는 아웃도어 운동 커뮤니티로 다양한 운동을 취향에 맞게 운동 클럽을 개설할 수 있습니다. 각자 운동 후 인증샷을 남기고, 운동에 관련한 이야기를 나눌 수 있는 커뮤니티 서비스를 제공하고 있습니다. 또한 자신만의 운동 스케줄을 작성하여 편리하게 관리할 수 있습니다.
운동끼리는 근처 동네의 운동 친구를 찾는 앱으로 가까운 거리에 비슷한 관심사를 가진 맞춤형 운동 친구를 추천해줍니다. 원한다면 관심있는 종목에 대해 운동모집을 만들 수도 있는 기능을 제공하고 있습니다.
<그림2> 페어플레이, 운동끼리 어플리케이션 화면
<그림3> 달력별 정보 예시
<그림4> 시간별, 장소별 정보 예시
<그림5> 회원별 정보 예시
<그림7> 실내암장 난이도 비교표 예시
📌 1주차 목표 (2.13 ~ 2.16)
React 개념 익히기
각자 프론트 만들어 보기
1) Node.js 설치
2) npm 설치
3) 에디터 설치 (Visual Studio Code 사용)
4) create-react-app 설치
(페이스북에서 만든 react project 생성 도구)
📌 2주차 목표 (2.17 ~ 2.22)
(공통) 서버에서 자료 받기
(민경) 한달 운동량 요약 + 메뉴바 상단
(석준) 운동 일정 (팝업으로 일정 확인, 운동 기록 확인) + 단계별 레벨 업
(수현) To Do List (입력 + 토글) + 로그인 구현
+ 암장 난이도 비교표
+ 일자별/장소별 운동 약속 확인
중앙에 박스와 타이틀이 보여지고, 하단에 폼과 리스트 배치
import React from 'react';
import './TodoListTemplate.css';
const TodoListTemplate = ({form, children}) => {
return (
<main className="todo-list-template">
<div className="title">
To Do List
</div>
<section className="form-wrapper">
{form}
</section>
<section className="todos-wrapper">
{ children }
</section>
</main>
);
};
export default TodoListTemplate;
input과 button 추가
import React from 'react';
import './Form.css';
const Form = ({value, onChange, onCreate, onKeyPress}) => {
return (
<div className="form">
<input value={value} onChange={onChange} onKeyPress={onKeyPress}/>
<div className="create-button" onClick={onCreate}>
추가
</div>
</div>
);
};
export default Form;
TodoItem 컴포넌트 여러개 렌더링
리스트 렌더링 시, 보여주는 리스트가 동적인 경우에는 함수형이 아닌 클래스형 컴포넌트로 작성하기 (컴포넌트 성능 최적화 가능)
import React, { Component } from 'react';
import TodoItem from './TodoItem';
class TodoItemList extends Component {
shouldComponentUpdate(nextProps, nextState) {
return this.props.todos !== nextProps.todos;
}
render() {
const { todos, onToggle, onRemove } = this.props;
const todoList = todos.map(
({id, text, checked}) => (
<TodoItem
id={id}
text={text}
checked={checked}
onToggle={onToggle}
onRemove={onRemove}
key={id}
/>
)
);
return (
<div>
{todoList}
</div>
);
}
}
export default TodoItemList;
체크 값이 활성화 되어 있으면, 우측에 체크마크 보여주기
마우스 위에 있을 때, 좌측에 x표시 보여주기 (x 클릭시 삭제)
컴포넌트 영역이 클릭되면, 체크박스 활성화되며 중간줄이 그어지기
import React, { Component } from 'react';
import './TodoItem.css';
class TodoItem extends Component {
shouldComponentUpdate(nextProps, nextState) {
return this.props.checked !== nextProps.checked;
}
render() {
const { text, checked, id, onToggle, onRemove } = this.props;
return (
<div className="todo-item" onClick={() => onToggle(id)}>
<div className="remove" onClick={(e) => {
e.stopPropagation(); // onToggle 이 실행되지 않도록 함
onRemove(id)}
}>×</div>
<div className={`todo-text ${checked && 'checked'}`}>
<div>{text}</div>
</div>
{
checked && (<div className="check-mark">✓</div>)
}
</div>
);
}
}
export default TodoItem;
import React, { Component } from 'react';
import TodoListTemplate from './Todolist/TodoListTemplate';
import Form from './Todolist/Form';
import TodoItemList from './Todolist/TodoItemList';
class App extends Component {
id = 3 // 이미 0,1,2 가 존재하므로 3으로 설정
state = {
input: '',
todos: [
{ id: 0, text: '클라이밍 서초 19시', checked: false },
{ id: 1, text: '아침 러닝 하기', checked: true },
{ id: 2, text: '사이클 30분 타기', checked: false },
]
}
handleChange = (e) => {
this.setState({
input: e.target.value // input 의 다음 바뀔 값
});
}
handleCreate = () => {
const { input, todos } = this.state;
this.setState({
input: '', // 인풋 비우고
// concat 을 사용하여 배열에 추가
todos: todos.concat({
id: this.id++,
text: input,
checked: false
})
});
}
handleKeyPress = (e) => {
// 눌려진 키가 Enter 면 handleCreate 호출
if(e.key === 'Enter') {
this.handleCreate();
}
}
handleToggle = (id) => {
const { todos } = this.state;
// 파라미터로 받은 id 를 가지고 몇번째 아이템인지 찾습니다.
const index = todos.findIndex(todo => todo.id === id);
const selected = todos[index]; // 선택한 객체
const nextTodos = [...todos]; // 배열을 복사
// 기존의 값들을 복사하고, checked 값을 덮어쓰기
nextTodos[index] = {
...selected,
checked: !selected.checked
};
this.setState({
todos: nextTodos
});
}
handleRemove = (id) => {
const { todos } = this.state;
this.setState({
todos: todos.filter(todo => todo.id !== id)
});
}
render() {
const { input, todos } = this.state;
const {
handleChange,
handleCreate,
handleKeyPress,
handleToggle,
handleRemove
} = this;
return (
<TodoListTemplate form={(
<Form
value={input}
onKeyPress={handleKeyPress}
onChange={handleChange}
onCreate={handleCreate}
/>
)}>
<TodoItemList todos={todos} onToggle={handleToggle} onRemove={handleRemove}/>
</TodoListTemplate>
);
}
}
export default App;
구글 로그인을 활용하기 위해서는 구글 클라우드 플랫폼을 이용해 OAuth2.0 클라이언트 ID를 발급받아야 함
OAuth : 인터넷 사용자들이 비밀번호를 제공하지 않고 구글, 카카오, 네이버, 페이스북 등에 저장되어 있는 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로써 사용되는 접근 위임 개방형 표준 프로토콜
애플리케이션으로서 구글 API를 호출하려면, 구글에 자신의 애플리케이션을 클라이언트로 등록하고 클라이언트 ID를 발급받아야함
프로젝트 선택 → 프로젝트 이름 → 만들기
API 및 서비스 → OAuth 동의 화면 → 외부 → 만들기
앱 이름 및 이메일 입력 → 저장
사용자 인증 정보 만들기 → OAuth 클라이언트 ID →
어플리케이션 유형 (웹 애플리케이션) → 승인된 자바스크립트 원본 (http://localhost:3000)
📖 참고 - 클라이언트 사이드 / 서버 사이드 구현 📖
$ npm install react-google-login
하단에 있는 원인으로 다른 라이브러리 모색
웹용 Google 로그인 자바스크립트 플랫폼 라이브러리가 지원 중단됩니다. 지원 중단 날짜인 2023년 3월 31일 이후에는 이 라이브러리를 다운로드할 수 없습니다. 대신 새로운 웹용 Google ID 서비스를 사용하세요.
새로 생성된 클라이언트 ID는 기본적으로 이전 플랫폼 라이브러리를 사용하지 못하도록 차단되며, 기존 클라이언트 ID는 영향을 받지 않습니다. 2022년 7월 29일 이전에 생성된 새 클라이언트 ID는 Google 플랫폼 라이브러리를 사용하도록 plugin_name을 설정할 수 있습니다.
authenticated 변수와 login 함수를 prop으로 받아서 사용하는 로그인 폼
이메일과 패스워드 입력 후 버튼을 클릭 시, 컴포넌트부터 prop으로 내려받은 login 함수를 호출해줍니다.
그러면 컴포넌트의 login 함수는 user state에 로그인된 사용자를 저장하거나, 예외를 발생킬 것입니다.
정상적으로 로그인이 되었다면 컴포넌트로 부터 넘어온 authenticated prop값은 true가 될 것입니다.
그러면 로그인 폼이 렌더링되는 대신에 React Router의 컴포넌트를 통해 로그인 이전에 접근하려고 했었던 페이지로 리다이렉션 됩니다.
import React, { useState } from "react";
import { Redirect } from "react-router-dom";
function LoginForm({ authenticated, login, location }) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleClick = () => {
try {
login({ email, password });
} catch (e) {
alert("Failed to login");
setEmail("");
setPassword("");
}
};
const { from } = location.state || { from: { pathname: "/" } };
if (authenticated) return <Redirect to={from} />;
return (
<>
<h1>Login</h1>
<input
value={email}
onChange={({ target: { value } }) => setEmail(value)}
type="text"
placeholder="email"
/>
<input
value={password}
onChange={({ target: { value } }) => setPassword(value)}
type="password"
placeholder="password"
/>
<button onClick={handleClick}>Login</button>
</>
);
}
export default LoginForm;
로그아웃 버튼은 컴포넌트로 부터 logout 함수를 prop으로 내려받습니다.
버튼 클릭 시 이 logout 함수를 호출하고, 사용자를 홈페이지로 이동시킵니다.
import React from "react";
import { withRouter } from "react-router-dom";
function LogoutButton({ logout, history }) {
const handleClick = () => {
logout();
history.push("/");
};
return <button onClick={handleClick}>Logout</button>;
}
export default withRouter(LogoutButton);
📌 3주차 목표 (2.23 ~ 3.3)
(공통) Django와 React 연동, React Router
(민경) 한달 운동량 요약
(석준) 단계별 레벨 표시
(수현) 로그인 구현
+ 암장 난이도 비교표
+ 일자별/장소별 운동 약속 확인
1) 터미널에서 설치
: python -m pip install Django
2) 서브 명령들 확인
: django-admin
3) 프로젝트 생성
: django-admin startproject (프로젝트 이름) (프로젝트 위치)
4) 실행
: python manege.py runserver (원하는 장고 서버)
1) 사용자가 다양한 경로로 접속하면 project의 urls.py에서 각각의 경로를 어떤 app에게 위임할건지 지정
2) app의 urls.py에서 적합한 view의 알맞은 함수로 위임됨
3) db에 직접 접속하지 않고 django에 model이라는 수단을 이용하여 db를 사용
4) db 정보를 받아서 client에게 html, json, xml 형태의 데이터를 만들어서 응답해줌
1) urls.py가 큰 틀의 routing 역할 수행
from django.contrib import admin
from django.urls import path
urlpatterns = [ # urlpatterns 반드시 정의
path('admin/', admin.site.urls), # routing과 관련된 정보 포함
# (admin/ - 장고가 기본적으로 가지고 있는 관리자 화면으로 이동하기 위한 routing 설정)
]
2) http://127.0.0.1에 접속했을 때 app의 views.py로 위임
# urls.py
from django.urls import path
from myapp import views
urlpatterns = [
path('', views.index), # 사용자가 home으로 들어옴 (아무것도 없는 경로)
path('create/', views.create),
path('read/<id>/', views.read)
]
# views.py
from django.shortcuts import render, HttpResponse
# index : client에게 정보를 전달하기 위한 함수
# request : 첫 번째 파라미터의 인자로 요청과 관련된 여러가지 정보가 담긴 객체 전달
def index(request):
return HttpResponse('Welcome!') # 처리한 결과를 return값으로 보내줌
def create(request):
return HttpResponse('Create!')
def read(request, id): # id를 이용하여 read함수의 파라미터를 이용해 변경 가능
return HttpResponse('Read'+id)
3) 접속이 들어올 때마다 랜덤한 정보를 동적으로 생성하기
from django.shortcuts import render, HttpResponse
import random
def index(request):
return HttpResponse('<h1>Random</h1>'+str(random.random())) # 앞은 문자열, 뒤는 숫자라서 오류 발생 -> str로 형변환
# (1) views.py 기본 틀
def index(request): # ''' 이용해 여러 줄 작성
return HttpResponse('''
<html>
<body>
<h1>Django</h1>
<ol>
<li>routing</li>
<li>view</li>
<li>model</li>
</ol>
<h2> Welcome</h2>
Hello, Django
</body>
</html>
''')
# (2) 각각의 data를 dictionary에 담고, 모아서 grouping 하기 위해 list에 묶기
topics = [ # dictionary에 data 담은 후 list로 그룹화
{'id':1, 'title':'routing', 'body':'Routing is ..'},
{'id':2, 'title':'view', 'body':'View is ..'},
{'id':3, 'title':'model', 'body':'Model is ..'},
]
def index(request): # ''' 이용해 여러 줄 작성
global topics # 해당 변수를 함수에서 사용하기 위해 전역변수로 지정
ol = ''
for topic in topics:
ol += f'<li><a href="/read/{topic["id"]}">{topic["title"]}</a></li>' # <a>태그로 상세페이지 링크 걸기
# f 붙이고 중괄호 사용시 변수 바로 사용 가능
return HttpResponse(f'''
<html>
<body>
<<h1><a href="/">Django</a></h1>
<ol>
{ol}
</ol>
<h2> Welcome</h2>
Hello, Django
</body>
</html>
''')
# (3) html코드 함수화 시키기
def HTMLTemplate(): # HTML 코드 함수화 시키기
global topics
ol = ''
for topic in topics:
ol += f'<li><a href="/read/{topic["id"]}">{topic["title"]}</a></li>'
return f'''
<html>
<body>
<h1><a href="/">Django</a></h1>
<ol>
{ol}
</ol>
<h2> Welcome</h2>
Hello, Django
</body>
</html>
'''
def index(request):
return HttpResponse(HTMLTemplate())
# (4) HTML태그 변수화 시키기 (index와 read는 본문의 내용이 달라져야해서 변수화)
def HTMLTemplate(article): # HTML 코드 함수화 시키기
global topics
ol = ''
for topic in topics:
ol += f'<li><a href="/read/{topic["id"]}">{topic["title"]}</a></li>'
return f'''
<html>
<body>
<h1><a href="/">Django</a></h1>
<ol>
{ol}
</ol>
{article}
</body>
</html>
'''
def index(request): # index와 read 본문 내용 달라야해서 변수화
article = '''
<h2> Welcome</h2>
Hello, Django
'''
return HttpResponse(HTMLTemplate(article)) # 본문의 내용을 인자로 전달
# (5) read 함수 구현
def read(request, id):
global topics
article = ''
for topic in topics:
if topic['id'] == int(id): # topic의 id는 정수, 파라미터 값은 문자열로 들어와 오류 발생
article = f'<h2>{topic["title"]}</h2>{topic["body"]}'
return HttpResponse(HTMLTemplate(article))
(1) create, delete, update 만들어야 하니까 리스트 형태로 변경
# views.py - HTMLTemplate 함수
def HTMLTemplate(article): # HTML 코드 함수화 시키기
global topics
ol = ''
for topic in topics:
ol += f'<li><a href="/read/{topic["id"]}">{topic["title"]}</a></li>'
return f'''
<html>
<body>
<h1><a href="/">Django</a></h1>
<ul>
{ol}
</ul>
{article}
<ul>
<li><a href="/create/">create</a></li>
</ul>
</body>
</html>
'''
(2) title, body 입력 및 제출 버튼 생성
# views.py - create 함수
def create(request):
# 입력값 이름 지정 : name, 도움말 : placeholder
# 줄바꿈을 위해 p태그 작성 (p : 단락 태그)
# 여러 줄 입력 : textarea
# title과 body에 담긴 type을 원하는 패스로 전달하기 위해 form 태그 사용
# from태그의 action 속성을 이용하여 원하는 패스로 전달 (/create/ : 현재 페이지로 전달)
article = '''
<form action="/create/">
<p><input type="text" name="title" placeholder="title"></p>
<p><textarea name="body" placeholder="body"></textarea></p>
<p><input type="submit"></p>
</form>
'''
브라우저가 서버에게 데이터 요청 시 아래 2가지 사용 (GET 방식)
?id=1 : query string (서버에게 정보를 질의할 때 사용)
브라우저가 서버에게 데이터 변경 시 GET 방식일 경우
(URL안에 입력값이 포함된 상태가 되면, 주소를 copy해서 공유시 사용자가 클릭할 때마다 글이 추가됨)
브라우저가 서버에 있는 데이터를 변경하려고 할 때 URL에 query string을 넣으면 안됨 (POST 방식)
(3) GET → POST 방식
# views.py - create 함수
# 브라우저가 서버에 있는 데이터를 변경시 POST 방식 사용
article = '''
<form action="/create/" method="post">
<p><input type="text" name="title" placeholder="title"></p>
<p><textarea name="body" placeholder="body"></textarea></p>
<p><input type="submit"></p>
</form>
'''
(4) GET 방식과 POST 방식 구분해서 실행
# views.py - create 함수
def create(request):
global nextId
if request.method == 'GET':
article = '''
<form action="/create/" method="post">
<p><input type="text" name="title" placeholder="title"></p>
<p><textarea name="body" placeholder="body"></textarea></p>
<p><input type="submit"></p>
</form>
'''
return HttpResponse(HTMLTemplate(article))
elif request.method == 'POST':
title = request.POST['title']
body = request.POST['body']
newTopic = {"id":nextId, "title":title, "body":body} # newTopic에 id값이 없어서 nextId 사용
topics.append(newTopic)
url = '/read/' + str(nextId)
nextId = nextId + 1
return redirect(url) # 누르면 상세보기 페이지 넘어가는 redirect 기능
(1) 해당 data 삭제하고 홈으로 이동
delete 페이지가 따로 작성되어 있지 않고, 서버의 데이터를 바로 수정해야 해서 POST 방식 사용
# views.py - HTMLTemplate 함수
def HTMLTemplate(article, id=None): # delete 함수에 어떤 id 삭제할지 필요한데, 인자를 사용하지 않으면 에러 발생 → id 인자 기본값 = None 지정
global topics
ol = ''
for topic in topics:
ol += f'<li><a href="/read/{topic["id"]}">{topic["title"]}</a></li>'
return f'''
<html>
<body>
<h1><a href="/">Django</a></h1>
<ul>
{ol}
</ul>
{article}
<ul>
<li><a href="/create/">create</a></li>
<li>
<form action="/delete/" method="post">
<input type="hidden" name="id" value={id}> // hidden : 눈에 보이지 않지만 서버에 데이터 전송
<input type="submit" value="delete">
</form>
</li>
</ul>
</body>
</html>
'''
# (2) id값 확인하여 일치한 것 삭제
# views.py - delete 함수
def delete(request):
global topics
if request.method == 'POST': # POST 방식이 맞는지 확인 필요
id = request.POST['id'] # id 값 가져오기
newTopics = [] # id가 일치하지 않는 것 추가
for topic in topics:
if topic['id'] != int(id):
newTopics.append(topic)
topics = newTopics
return redirect('/') # 삭제 후 홈으로 이동
(3) 홈페이지가 아닌 상세페이지에 있을 때만 삭제 버튼 활성화
# views.py - HTMLTemplate 함수
def HTMLTemplate(article, id=None):
global topics
contextUI = '' # 맥락에 따라서 UI가 만들어지고 안만들어지고 정해짐
if id != None: # id값을 가지고 있다면 상세페이지에 위치, 없다면 홈에 위치
contextUI = f'''
<li>
<form action="/delete/" method="post">
<input type="hidden" name="id" value={id}>
<input type="submit" value="delete">
</form>
</li>
'''
ol = ''
for topic in topics:
ol += f'<li><a href="/read/{topic["id"]}">{topic["title"]}</a></li>'
return f'''
<html>
<body>
<h1><a href="/">Django</a></h1>
<ul>
{ol}
</ul>
{article}
<ul>
<li><a href="/create/">create</a></li>
{contextUI}
</ul>
</body>
</html>
'''
(1) Create와 유사하지만, 차이점은 form 안에 데이터가 표시됨
# views.py - HTMLTemplate 함수
if id != None: # id값을 가지고 있다면 상세페이지에 위치, 없다면 홈에 위치
contextUI = f'''
<li>
<form action="/delete/" method="post">
<input type="hidden" name="id" value={id}>
<input type="submit" value="delete">
</form>
</li>
<li><a href="/update/{id}">update</a></li>
'''
# views.py - update 함수
def update(request, id):
global topics
if request.method == 'GET': # GET으로 접속시 update 텍스트 출력
for topic in topics:
if topic['id'] == int(id): # 조회 성공시 selectedTopic 딕셔너리에 담기
selectedTopic = {
"title":topic['title'],
"body":topic['body']
}
article = f'''
<form action="/update/{id}/" method="post">
<p><input type="text" name="title" placeholder="title" value={selectedTopic['title']}></p>
<p><textarea name="body" placeholder="body">{selectedTopic['body']}</textarea></p>
<p><input type="submit"></p>
</form>
'''
return HttpResponse(HTMLTemplate(article, id))
elif request.method == 'POST': # POST로 데이터 수정시 상세보기페이지로 이동
title = request.POST['title']
body = request.POST['body']
for topic in topics:
if topic['id'] == int(id): # id 같을 경우 값 수정
topic['title'] = title
topic['body'] = body
return redirect(f'/read/{id}')
1) django template에서 react가 작동할 수 있게 static 파일에 react.js를 넣어 라이브러리로 사용
→ 서버 구조 간단하지만 react 기능 사용에 제한이 많아 채택x
2) 프론트엔드를 react로 작성하고, 데이터는 내부통신망의 django-rest-framework를 이용하여 가져오는 방식
1) backend 디렉터리
# settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'post', # api로 호출할 app 이름 추가
]
# models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
def __str__(self):
"""A string representation of the model."""
return self.title
# admin.py
from django.contrib import admin
from .models import Post
admin.site.register(Post)
2) migrate으로 변경된 부분 db에 적용
3) post 추가
# settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'post', # api로 호출할 app
'rest_framework', # django-rest-framework 추가
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
]
}
📌 목표