본문 바로가기
IT & 개발공부/웹개발(HTML, CSS)

장고 프로젝트(맛집 공유 사이트 만들기) - 4 CRUD(맛집정보) Django Project Restaurant share

by 규딩코딩 2023. 8. 1.

2023.07.31 - [IT & 개발공부/웹개발(HTML, CSS)] - 장고 프로젝트(맛집 공유 사이트 만들기) - CRUD(카테고리) Django Project Restaurant share - 3

 

장고 프로젝트(맛집 공유 사이트 만들기) - CRUD(카테고리) Django Project Restaurant share - 3

2023.07.30 - [IT & 개발공부/웹개발(HTML, CSS)] - 장고 프로젝트(맛집 공유 사이트 만들기) - 템플릿 설정하기 Django Project Restaurant share - 2 장고 프로젝트(맛집 공유 사이트 만들기) - 템플릿 설정하기 Djan

bmil2011s.tistory.com

지난 글에서는 공유 사이트의 기능을 크게 두 가지로 나눴을 때의 1번 Category 에 대한 CR(U)D 기능을 구현하였다. 이어서 2번 Restaurant 정보와 관련한 CRUD기능을 구현하고자 한다.

 

1. Create

- 먼저 메인화면에서 맛집 추가 버튼을 누르면 해당 화면이 나오게 되는데, 카테고리가 드롭다운 형식으로 나타나고 있지만 실제 데이터베이스에 존재하는 카테고리가 보이지 않고 있다. html 템플릿과 views.py 파일을 수정해주자.

- 이후 restaurantCreate.html에서 95번째 줄부터 106번째줄을 다음과 같이 수정해주자.

- 이후부터는 우리가 이전에 추가해 주었던 카테고리들이 나타날 것이다.

- 추가로 첫 화면에서 첫 번째 카테고리 항목이 아니라  기본 그룹 카테고리가 선택된 것으로 보여주도록 selected를 붙여주고, if 문을 이용하면 해당 기능이 구현된다.

                    <div class="input-group">
                        <span class="input-group-addon" id="sizing-addon2">카테고리</span>
                        <select id="resCategory" name="resCategory" class="resCategory" size="1" required autofocus>
                            {% for category in categories %}
                            {% if category.id == 3 %}
                            <option value="{{category.id}}" selected>{{category.category_name}}</option>
                            {% else %}
                            <option value="{{category.id}}">{{category.category_name}}</option>
                            {% endif %}
                            {% endfor %}
                        </select>  
                    </div>

- 실질적 Create 기능 구현 이전에 저번 Category 경우처럼 restaurant에 대한 model 정의 및 데이터베이스 반영이다. 맛집 추가화면을 봤을 때 하나의 restaurant가 가져야하는 정보는 카테고리/이름/링크/내용/장소키워드 총 5개이다. 이에 관한 내용을 models.py에 Restaurant라는 이름의 Class로 정의해주자.

- 분량은 길어졌지만, 내용이 추가적으로 어려워진 것은 아니고 그나마 생소한 것은 ForeignKey 요소다.

 

* ForeignKey는 해당 정보가 단순 문자열 또는 숫자를 가지는 의미를 넘어 다른 모델을 참조한다는 것을 의미한다. 이번 사례를 통해 이해를 해보자면 해당요소는 Category 모델을 참조(=아무 값이나 오는 것이 아니고 참조하는 모델에 존재하는 데이터만 값으로 받는 것)하고, on_delete는 참조하는 모델이 삭제되었을 때 해당 요소는 어떻게 해야할지에 대해 묻는 것인데 이번의 SET_DEFAULT 옵션은 참조 값을 설정해 둔 DEFAULT값으로 설정하겠다 이고, 앞서 해당 모델 중 알아낸 3번은 기본그룹이기 때문에, 기본 그룹을 참조하게된다.

- 모델을 정의한 이후에는 python manage.py makemigrations 명령어와 python manage.py migrate로 실제 데이터베이스에 반영해주도록 하자.

- 그 다음은 Crate 기능 로직 구현을 위하여 먼저 views.py 에서 함수를 추가해주도록 하자.

def Create_restaurant(request):
    category_id = request.POST['resCategory']
    category = Category.objects.get(id = category_id)
    name = request.POST['resTitle']
    link = request.POST['resLink']
    content = request.POST['resContent']
    keyword = request.POST['resLoc']
    new_res = Restaurant(category=category, restaurant_name=name, restaurant_link = link, restaurant_content=content, restaurant_keyword=keyword)
    new_res.save()
    return HttpResponseRedirect(reverse('index'))

- url이 views.py 의 해당 함수로 도착할 수 있으려면 템플릿과 urls.py도 수정해주자.

- 템플릿에서는 form action값을 다음과 같이 설정

- urls.py에서는 다음과 같이 9번 줄을 추가

템플릿
urls.py

create 기능이 잘 구현되었다면, 맛집 추가하기 화면에서 내용을 입력하고 "맛집 추가!" 버튼을 눌렀을 때 메인 화면으로 돌아가면 정상임을 확인할 수 있다.

 

2. Read

- 메인 화면에서 각 카데고리를 누르면 나오는 체크박스 1,2의 자리에 각 카테고리에 해당하는 맛집들을 보여주려고 한다.

- 먼저 보여줄 데이터를 가져와야하므로 views.py에서 index함수를 다음과 같이 수정하자.(많이 봤던 딕셔너리 불러오기 방식)

- 다음으로는 index.html 템플릿을 수정해주자 223번째 줄과 235번째 줄을 수정하였으며, 아래의 코드는 설명의 편의상 218번 ul class 부터 236 /ul 까지 가져온 것이다.

218                        <ul class="restaurantListDiv nav nav-pills nav-stacked">
                            {% for category in categories %}
                            <li class="Category deactive">{{ category.category_name }}</li>
                            <ul class="restaurantList">

                                {% for restaurant in restaurants %}
                                {% if restaurant.category == category %}
                                <div class="input-group">
                                    <span class="input-group-addon">
                                        <input name="checks" id="check{{restaurant.id}}" type="checkbox" value="{{restaurant.id}}">
                                    </span>
                                    <a href="restaurantDetail/{{restaurant.id}}"><input name="res{{restaurant.id}}" id="res{{restaurant.id}}" type="text" class="form-control" disabled style="cursor: pointer;" value="{{restaurant.restaurant_name}}"></a>
                                </div>
                                {% endif %}
                                {% endfor %}

                            </ul>
                            {% endfor %}
236                        </ul>

* 코드 설명 : 219번 줄에 의해 for 반복문에서 하나의 category가 꺼내지고 그 값과 223 줄에 의해 꺼내진 모든 restaurants를 꺼내어 224 줄 if문으로 해당 값들을 일치하는지 확인한다. 일치한다면 225줄부터 229번 줄을 출력함.

* 227 줄에서 restaurant.id를 동일하게 준 것은 추후 이메일 발송 때 선택한(체크된) 맛집들을 한 번에 받아 내기 위해서이다.

* 229 줄은 해당 restaurant를 눌렀을 때 상세 내용을 확인하기 위해 a href와 input name 에 restaurant.id를 붙여 주었다. 또한 user에게 어떤 restaurant인지 보여주기 위해 value 값은 restraurant의 이름으로 설정해주었다.

 

- 다음으로 restaurant를 눌렀을 때 표시될 상세 페이지에 대한 수정이 필요하다. 상세 페이지를 보여 주려면 어떠한 restaurant을 선택했을 때 정확히 그 restaurant에 관한 정보를 보여줘야하는 기능이 필요한데 이미 url에 담겨있는 것을 확인할 수 있다. 바로 위의 템플릿에서 229 줄을 참고해보면된다.

* 이론적으로 정확한 로직은 아니지만 이해할 때, user가 보는 restaurant의 이름, 그리고 클릭>> 그 이름을 가진 restaurant id >> 그 id의 상세 정보 페이지로 이동하는 느낌으로 생각하면 되겠다.

 

- 이제 남은 건 html에 맞게 urls와 views 파일을 수정해주는 것이다.

- urls.py 파일을 다음과 같이 수정해주자.

* <str:res_id> : 꺾쇠(<>)로 표현된 것은 동적인 값을 의미, 콜론을 중심으로 좌측에는 어떤 데이터 타입으로 받을지, 우측은 어떤 이름으로 받을지를 결정해준다.

- 다음으로 views 의 해당 함수도 다음과 같이 수정해주자.

- 함수에서 매개 변수로 res_id를 추가해 주었는데, url을 통해서 오는 값은 GET방식이 아니라면, request에 담기지 않아 이와 같이 추가적인 매개 변수로 받아 내야 서버에서 처리 가능하기 때문이다.

- 코드 내용은 url로 받은 res_id를 통해 실제 해당 id를 가지는 Restaurant 데이터를 가져오고 restaurant 변수에 담아 content 딕셔너리에 저장해준다는 내용이다.

- 딕셔너리를 다시 템플릿으로 전달했으니, 정보를 직접 보여주기 위해서 템플릿을 수정해주도록 하자.

- 살펴볼만한 내용으로는 96번 줄에서 restaurant.category는 데이터 그 자체이기 때문에 category name을 출력하려면 온점을 하나 더 추가해서 다음과 같이 접근해야 한다.

- 그리고 108번 줄에서textarea 태그이므로 다른 값들과 달리 꺾쇠가 끝나는 부분 이후에 적어줘야한다.

- 지금까지 잘 작성해주었다면, 웹페이지에서 추가와 상세페이지 까지 잘 연결될 것이다.

 

3. Update

- 이번에도 먼저 정확하게 어떤 기능을 만들고자 하는지 생각해보자.

- 먼저 수정 버튼이 있어야하므로, 상세보기 페이지에서 홈으로 버튼 옆에 수정하기 버튼을 추가함.

- 수정 버튼을 누르면 맛집 추가하기 화면과 동일한 화면이 나오도록, 단지 기존 내용 유지됨.

- 수정 완료 버튼을 누르면 수정 내용은 서버로 전달되어 데이터 값을 수정, 그 후에는 다시 restaurant에 대한 맛집 상세보기 화면을 User에게 표출

 

- 먼저 맛집 상세보기 페이지 템플릿에서 수정하기 버튼을 추가하도록 다음과 같이 코드를 수정해주자.

- 템플릿에 새로운 url이 추가되었으니, 다음으로 urls.py로 향하여 8번 줄과 같이 추가

- 새로운 기능(함수)이 구현되었으니, 다음은 views.py에서 다음과 같이 수정해주자.

- 20번 코드를 이용하여 먼저 받아온 res_id를 통해 일치하는 restaurant 기존 데이터를 가져오는 기능이 추가 되었다.

- 기존 이론과 크게 다른 것은 없으나, 22번 줄의 처음보는 템플릿이 눈에 띈다. Update 기능을 구현하기 위해서는 이전에 말했던 것처럼 create는 아니지만 create와 거의 유사한 형태의 페이지가 필요하다는 것을 짐작했을 것이다.

- 새로운 페이지가 필요하므로 다 새로 만들수도 있겠지만, 지혜롭게 restaurantCreate 복사하여 사용해보자. 파일을 복사하여 템플릿 이름을 restaurantUpdate로 바꿔준 뒤 다음과 같이 코드를 수정하자.

- 또한, views.py에서 템플릿으로 데이터들을 전달하였으므로 실질적인 기능 구현을 위해 추가적으로 템플릿을 대대적으로 수정해주자.

 

-99번 줄에서 기존에는 category id가 3인지(기본 설정)인지를 비교했다. 하지만 Update 화면에서는 수정하려는 restaurant가 속한 category가 선택된 상태가 돼야 하므로, 다음과 같이 수정하자.

- 109, 113, 12번 줄에서는 각각 input 태그의 value 속성을 새로 만들어 restaurant의 알맞은 값을 넣어주었고, 117번 줄에서는 textarea 태그이므로 꺾쇠가 끝나는 부분에 수정/추가 하였다.

- 이렇게 수정하면 수정하기 화면에서 기존 정보가 입력된 것을 확인할 수 있다. 이후 실제 update 기능을 구현해 볼 것이다.

            <form action="./create" method="POST" onsubmit="return checkFrom();">{% csrf_token %}
                <div class="inputDiv">
                    <div class="input-group">
                        <span class="input-group-addon" id="sizing-addon2">카테고리</span>
                        <select id="resCategory" name="resCategory" class="resCategory" size="1" required autofocus>
                            {% for category in categories %}
                            {% if category == restaurant.category %}
                            <option value="{{category.id}}" selected>{{category.category_name}}</option>
                            {% else %}
                            <option value="{{category.id}}">{{category.category_name}}</option>
                            {% endif %}
                            {% endfor %}
                        </select>  
                    </div>
                    <div class="input-group">
                        <span class="input-group-addon" id="sizing-addon2">맛집 이름</span>
                        <input id="resTitle" name="resTitle" type="text" class="form-control" placeholder="맛집 이름을 입력해주세요." aria-describedby="sizing-addon2" value="{{restaurant.restaurant_name}}">
                    </div>
                    <div class="input-group">
                        <span class="input-group-addon" id="sizing-addon2">관련 링크</span>
                        <input id="resLink" name="resLink" type="text" class="form-control" placeholder="관련 링크를 입력해주세요." aria-describedby="sizing-addon2" value="{{restaurant.restaurant_link}}">
                    </div>
                    <div class="input-group">
                        <span class="input-group-addon" id="sizing-addon2">상세 내용</span>
                        <textarea id="resContent" name="resContent" cols="90" rows="10" placeholder="상세 내용을 입력해주세요.">{{restaurant.restaurant_content}}"</textarea>
                    </div>
                    <div class="input-group">
                        <span class="input-group-addon" id="sizing-addon2">장소 키워드</span>
                        <input id="resLoc" name="resLoc" type="text" class="form-control" placeholder="장소 키워드를 입력해주세요." aria-describedby="sizing-addon2" value="{{restaurant.restaurant_keyword}}">
                    </div>
                    <input type="submit" class="resAddBtn btn btn-info" role="button" value="맛집 추가!"/>
                </div>
            </form>

- 실제 Update 기능을 위해 무엇이 필요한지 생각해보자.

먼저 User는 맛집 수정하기 화면에서 수정하기 버튼을 클릭해야 하는데, 현재 맛집 수정하기 화면에 들어가면 맛집 추가 버튼만 있고 수정하기 버튼이 없다.

수정하기 버튼을 클릭하면 update 하려는 url로 가도록 action 값을 수정해야한다.

- 또한, User가 수정하기 버튼을 눌러서 특정 url로 가서 ~  views의 특정 함수에서 ~ update기능을 처리하는 과정을 생각했을 때 필요한 것이 한가지 더 있는데 바로 restaurant의 id 값을 넘겨주는 것이다. 이를 위해서 User에게는 보이지 않지만 값을 전달할 수 있는 hidden 타입 input을 이용하자도록 하자.

 

- 위 세가지를 html 파일에서 다음과 같이 수정하자. 93번 줄 action 값을 ./update로, 123번 줄을 새로 추가, 124번 줄의 문자열을 맛집 수정! 으로 

            <form action="./update" method="POST" onsubmit="return checkFrom();">{% csrf_token %}
                <div class="inputDiv">
                    <div class="input-group">
                        <span class="input-group-addon" id="sizing-addon2">카테고리</span>
                        <select id="resCategory" name="resCategory" class="resCategory" size="1" required autofocus>
                            {% for category in categories %}
                            {% if category == restaurant.category %}
                            <option value="{{category.id}}" selected>{{category.category_name}}</option>
                            {% else %}
                            <option value="{{category.id}}">{{category.category_name}}</option>
                            {% endif %}
                            {% endfor %}
                        </select>  
                    </div>
                    <div class="input-group">
                        <span class="input-group-addon" id="sizing-addon2">맛집 이름</span>
                        <input id="resTitle" name="resTitle" type="text" class="form-control" placeholder="맛집 이름을 입력해주세요." aria-describedby="sizing-addon2" value="{{restaurant.restaurant_name}}">
                    </div>
                    <div class="input-group">
                        <span class="input-group-addon" id="sizing-addon2">관련 링크</span>
                        <input id="resLink" name="resLink" type="text" class="form-control" placeholder="관련 링크를 입력해주세요." aria-describedby="sizing-addon2" value="{{restaurant.restaurant_link}}">
                    </div>
                    <div class="input-group">
                        <span class="input-group-addon" id="sizing-addon2">상세 내용</span>
                        <textarea id="resContent" name="resContent" cols="90" rows="10" placeholder="상세 내용을 입력해주세요.">{{restaurant.restaurant_content}}"</textarea>
                    </div>
                    <div class="input-group">
                        <span class="input-group-addon" id="sizing-addon2">장소 키워드</span>
                        <input id="resLoc" name="resLoc" type="text" class="form-control" placeholder="장소 키워드를 입력해주세요." aria-describedby="sizing-addon2" value="{{restaurant.restaurant_keyword}}">
                    </div>
                    <input type="hidden" id="resId" name="resId" value="{{restaurant.id}}"/>
                    <input type="submit" class="resAddBtn btn btn-info" role="button" value="맛집 수정!"/>

- url 이 변경되었으므로, urls.py를 다음과 8번 줄을 추가하자. 이때 8번 줄을 9번줄보다 아래에 작성하게되면 에러*가 발생한다. update로 끝나는 url이 <str:res_id>로 끝나는 url보다 위에 적어야하는 것이다.

* 쉽게 이해하자면 <stre:res_id>이 흡수력이 좋아서 조심해야한달까, 뭐든지 들어갈 수 있으니.

- 다음으로  urls.py에서 새로 정의한 함수를 views.py에 추가해주자. 코드가 갑자기 왕창 많은 함수가 등장했는데 사실 같은 내용의 반복이라 큰 어려움은 없을 것이다. 25~31번까지가 User에게 값을 받는 코드이다. 27번 줄에서는 hidden 타입의 input 요소로부터 수정하려는 restaurant의 id를 가져오고 이를 통해 33번 줄에서 실제 restaurant 데이터로 가져온다.

이후 34번 줄부터 38번 줄까지 restaurant데이터에 대해 이전의 값을 사용자가 입력한 값으로 바꿔주고, 38번 줄에서 저장하는 과정이다. 47번 줄에서 kwargs라는 값이 나오는데, 이는 resDetailPage라는 이름을 갖는 url 뒷부분에 <str:res_id> 부분을 채워 주는 역할을 수행한다.

4. Delete

- Delete는 맛집 상세보기 화면에서 버튼이 작동하도록 하겠다. 수정하기와 약간 다른 것은 수정하기 버튼은 단순 url 변경만 이뤄지도록 하는 버튼이지만, 삭제하기 버튼은 누름과 동시에 url 변경뿐 아니라 서버로 어떤 restaurant가 삭제돼야 하는지 정보가 함께 전달되어야 하기 때문이다. 이 두가지는 url 처리 때 파라미터 값을 주느냐 안주냐 차이기도 하다. 한편 정보를 같이 전달하는 방법에는 GET과 POST 방식이 있는데, Delete기능은 데이터베이스의 데이터에 대한 조작이 이뤄지므로 POST 방식으로 처리할 것이다.

- 먼저 html 파일에 다음과 같이 코드를 추가하자. a 태그가 아닌 방식으로 url을 이동시켜주기 위해 from 태그를 사용했고, form 태그 내부에 함께 전달해야 하는 restaurant의 id 값을 hidden 타입의 input으로 추가하여 User에게는 보이지 않는다.

- submit 타입의 input요소를 추가해 주었는데, 이 요소는 버튼과 같이 User에게 보이며, 해당 요소를 클릭 때 form 태그 action 값으로 url이 이동되도록 하는 요소다.

- url이 새로 생겼으므로 urls.py로 가서 다음과 같이 코드를 추가하자. 이전에 설명한 것처럼 8번 줄보다 반드시 상위에 추가해야한다.

- 다음으로 추가한 함수를 실제로 views.py에 구현하자.

- request.POST 를 통해 받은 resId값을 res_id 변수로 받고 그 값을 통해 restaurant모델에서 실제 데이터를 찾고, 해당 데이터를 delete() 함수로 삭제해주는 코드이다. 삭제가 끝나면 index(메인)페이지로 돌아간다.

 

 

 

 

 

* 이 글은 Django 한그릇 뚝딱(문범우, 2019)의 챕터 3 "맛집 공유 사이트 만들기" 내용을 참고하였음(틀린 부분 약간 수정됨).

반응형