๐ HTML Form๊ณผ ์ฅ๊ณ Form์ ํ์ฉํ ์ฑํ ๋ฉ์์ง ์ ์ก ๋ฐ ์๋ตยถ
- ๋ณ๊ฒฝ ํ์ผ์ ํ ๋ฒ์ ๋ฎ์ด์ฐ๊ธฐ ํ์ค๋ ค๋ฉด, pyhub-git-commit-apply ์ ํธ๋ฆฌํฐ ์ค์นํ์ ํ์, ํ๋ก์ ํธ ๋ฃจํธ์์ ์๋ ๋ช ๋ น ์คํํ์๋ฉด ์ง์  ์ปค๋ฐ์ ๋ชจ๋ ํ์ผ์ ๋ค์ด๋ฐ์ ํ์ฌ ๊ฒฝ๋ก์ ๋ฎ์ด์ฐ๊ธฐํฉ๋๋ค. 
python -m pyhub_git_commit_apply https://github.com/pyhub-kr/django-webchat-rag-langcon2025/commit/5f2095926ad34426876cb56e4f5364d6cde83d47
uv๋ฅผ ์ฌ์ฉํ์ค ๊ฒฝ์ฐ
uv run pyhub-git-commit-apply https://github.com/pyhub-kr/django-webchat-rag-langcon2025/commit/5f2095926ad34426876cb56e4f5364d6cde83d47
์ฑํ ๋ฉ์์ง ์ ์ก ๋ทฐยถ
์๋ก์ด ์ฑํ ๋ฉ์์ง๋ฅผ ์ ์ก๋ฐ์ View๋ฅผ ์ ํต์ ์ธ ์ฅ๊ณ Form ํจํด์ผ๋ก ๊ตฌํํฉ๋๋ค.
- HTML ํผ ์์ฒญ์์๋ - GET์์ฒญ๊ณผ- POST์์ฒญ 2๊ฐ์ง๋ง ์กด์ฌํฉ๋๋ค.- GET์์ฒญ์ ์กฐํ ๋ชฉ์ ์ผ๋ก๋ง ์ฌ์ฉํ๋ฉฐ, ์์ฑ/์์ /์ญ์  ์์ฒญ์ ํญ์- POST์์ฒญ์ผ๋ก ๋ฐ์ต๋๋ค.
- ์์ฒญ์ ํ์ผ ๋ฐ์ดํฐ๋ - request.FILES์์ฑ์ ํตํด ์ฐธ์กฐํ ์ ์์ผ๋ฉฐ, ๊ทธ ์ธ POST ๋ฐ์ดํฐ๋- request.POST์์ฑ์ ํตํด ์ฐธ์กฐํ ์ ์์ต๋๋ค.
- ์์ฒญ ๋ฐ์ดํฐ์ ๋ํ ์ ํจ์ฑ ๊ฒ์ฌ๋ ์ฅ๊ณ Form ์ธ์คํด์ค๋ฅผ ์์ฑํ๊ณ , - .is_valid()๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ ์ํํฉ๋๋ค. ๋จ ํ๋์ ์ ํจ์ฑ ๊ฒ์ฌ๋ผ๋ ์คํจํ๋ฉด- False๋ฅผ ๋ฐํํฉ๋๋ค.- ๐ฅ ์ค์: ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ์ ๋ฌ๋ฐ์ ๋ฐ์ดํฐ๋ ์ ๋ ์ ๋ขฐํด์๋ ์ ๋ฉ๋๋ค. ๋น์ฐํ ์ ๋ง์ถฐ ์ ๋ฌํ์ ๊ฒ์ด๋ผ ๊ฐ์ ํด์๋ ์ ๋ฉ๋๋ค. ๋ฐ๋์ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์ํํ์ฌ ๋ฐ์ดํฐ ๊ฐ/ํฌ๋งท/ํ์ ๋ฑ์ ํ์ธํด์ผ ํฉ๋๋ค. ํ๋ก ํธ์๋ ๋จ์์ ์ ๋ ฅ๊ฐ์ ์ ๊ตฌ์ฑํด์ ๋ณด๋๋คํ๋๋ผ๋, ๋๊ตฐ๊ฐ ์ ์์ ์ธ ๋ชฉ์ ์ผ๋ก ์ค๊ฐ์ ๊ฐ์ ๋ณ์กฐํ ์ ์์ต๋๋ค. ์ฅ๊ณ Form์ ํตํด ํจ์จ์ ์ผ๋ก ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์ํํ ์ ์์ต๋๋ค. 
 
- ์ ํจ์ฑ ๊ฒ์ฌ์ ํต๊ณผํ๋ฉด, ๋ชจ๋ธ ํผ์ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๊ณ , AI ๋ฉ์์ง๋ฅผ ์์ฑํ ํ์ ์ฑํ ๋ฐฉ ํ์ด์ง๋ก ์ด๋์ํต๋๋ค. - 1๊ฐ์ ์ฑํ ๋ฉ์์ง๋ฅผ ๋ฐ๊ณ ํ์ด์ง๋ฅผ ์ด๋์ํค๋ UI๊ฐ ์ข์ ๊ฒฝํ์ ์๋๋๋ค. ํ์ง๋ง ์ด๋ ์ฅ๊ณ ๋ฅผ ํจ์จ์ ์ผ๋ก ํ์ฉํ ์์ฐ์ฑ ๋์ ๊ฐ๋ฐ ๋ฐฉ๋ฒ์ ๋๋ค. 
- ๋ค์ โฃ๏ธ HTMX๋ฅผ ํตํด JS์์ด ๋ชจ๋ ์น ์ฑํ UI ๋ฌธ์์์ ์๋ฐ์คํฌ๋ฆฝํธ ์์ด๋ ์ฅ๊ณ ์ค์ฌ์ผ๋ก ํจ์จ์ ์ผ๋ก UX๋ฅผ ํฅ์์ํฌ ์ ์๋ ๋ฐฉ๋ฒ์ ์๊ฐํฉ๋๋ค. 
 
- ์ ํจ์ฑ ๊ฒ์ฌ์ ์คํจํ๋ฉด, ์๋ฌ ๋ฉ์์ง์ ํจ๊ป ์๋ฌ HTML ํผ ํ๋ฉด์ ์๋ตํฉ๋๋ค. 
chat/views.py ํ์ผ ๋ฎ์ด์ฐ๊ธฐยถ 1from django.shortcuts import get_object_or_404, render, redirect
 2from django.urls import reverse_lazy
 3from django.views.decorators.http import require_POST
 4from django.views.generic import ListView, CreateView
 5
 6from .forms import RoomForm, MessageForm
 7from .models import Room, TaxLawDocument
 8
 9
10# ์ฑํ
๋ฐฉ ๋ชฉ๋ก ํ์ด์ง (ํด๋์ค ๊ธฐ๋ฐ ๋ทฐ)
11room_list = ListView.as_view(model=Room)
12
13
14# ์ ์ฑํ
๋ฐฉ ์์ฑ ํ์ด์ง (ํด๋์ค ๊ธฐ๋ฐ ๋ทฐ)
15room_new = CreateView.as_view(
16    model=Room,
17    form_class=RoomForm,
18    success_url=reverse_lazy("chat:room_list"),
19)
20
21
22# ์ฑํ
๋ฐฉ ์ฑํ
 ํ์ด์ง (ํจ์ ๊ธฐ๋ฐ ๋ทฐ)
23def room_detail(request, pk):
24    # ์ง์  ์ฑํ
๋ฐฉ ์กฐํํ๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์์ผ๋ฉด 404 ์ค๋ฅ ๋ฐ์
25    room = get_object_or_404(Room, pk=pk)
26    # ์ง์  ์ฑํ
๋ฐฉ์ ๋ชจ๋  ๋ํ ๋ชฉ๋ก
27    message_list = room.message_set.all()
28    return render(
29        request,
30        "chat/room_detail.html",
31        {
32            "room": room,
33            "message_list": message_list,
34        },
35    )
36
37
38# ๋ฌธ์ ๊ฒ์ ํ์ด์ง
39class TaxLawDocumentListView(ListView):
40    model = TaxLawDocument
41    # sqlite์ similarity_search ๋ฉ์๋๊ฐ ์ฟผ๋ฆฌ์
์ด ์๋ ๋ฆฌ์คํธ๋ฅผ ๋ฐํํ๊ธฐ ๋๋ฌธ์
42    # ListView์์ ํ
ํ๋ฆฟ ์ด๋ฆ์ ์ฐพ์ง ๋ชปํ๊ธฐ์ ์ง์  ์ง์ ํด์ค๋๋ค.
43    template_name = "chat/taxlawdocument_list.html"
44
45    def get_queryset(self):
46        qs = super().get_queryset()
47
48        query = self.request.GET.get("query", "").strip()
49        if query:
50            qs = qs.similarity_search(query)  # noqa: list ํ์
51        else:
52            # ๊ฒ์์ด๊ฐ ์๋ค๋ฉด ๋น ์ฟผ๋ฆฌ์
์ ๋ฐํํฉ๋๋ค.
53            qs = qs.none()
54
55        return qs
56
57
58# POST ์์ฒญ ๋ง์ ํ์ฉํฉ๋๋ค.
59@require_POST
60def message_new(request, room_pk):
61    room = get_object_or_404(Room, pk=room_pk)
62
63    form = MessageForm(data=request.POST, files=request.FILES)
64    if form.is_valid():
65        message = form.save(commit=False)
66        message.room = room
67        message.save()
68        # ๋ํ ๋ชฉ๋ก์ ๊ธฐ๋ฐํด์ AI ์๋ต ์์ฑํ๊ณ  ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํฉ๋๋ค.
69        # ๋ฐฉ๊ธ ์
๋ ฅ๋ ์ ์  ๋ฉ์์ง๊ฐ ๋ํ ๊ธฐ๋ก ๋ง์ง๋ง์ ์ถ๊ฐ๋์ด ์์ต๋๋ค.
70        ai_message = room.create_ai_message()
71        return redirect("chat:room_detail", pk=room_pk)
72
73    return render(
74        request,
75        "chat/message_form.html",  # ์์ฑํ์ง ์์ ํ
ํ๋ฆฟ.
76        {
77            "room": room,
78            "form": form,
79        },
80    )
๋ฐฉ๊ธ ๊ตฌํํ message_new ๋ทฐ๋ฅผ ํธ์ถํ๋ URL ํจํด์ ์ถ๊ฐํฉ๋๋ค.
chat/urls.py ํ์ผ ๋ฎ์ด์ฐ๊ธฐยถ 1from django.urls import path
 2from . import views
 3
 4app_name = "chat"
 5
 6urlpatterns = [
 7    path("", views.room_list, name="room_list"),
 8    path("new/", views.room_new, name="room_new"),
 9    path("<int:pk>/", views.room_detail, name="room_detail"),
10    path("<int:room_pk>/messages/new/", views.message_new, name="message_new"),
11    path("docs/law/tax/", views.TaxLawDocumentListView.as_view()),
12]
๊ฐ์ํํ room_detail.html ํ ํ๋ฆฟ ์ฝ๋ยถ
์ด์  ๋ฌธ์์์ ์ฌ์ฉํ ํ ํ๋ฆฟ์ ์คํ์ผ์ด ๋ณต์กํด์ ์ฝ๋๋ฅผ ๊ฐ์ํํ์ฌ ์์ ๋ฅผ ์งํํ๊ฒ ์ต๋๋ค.
- <form>ํ๊ทธ์- method="post"์์ฑ์ ์ถ๊ฐํ์ฌ- POST๋ฐฉ์์ผ๋ก ์์ฒญ์ ์ ์กํ๊ณ ,
- action์์ฑ์ผ๋ก ๋ฉ์์ง๋ฅผ ์ ์กํ ์ฃผ์๋ฅผ ์ง์ ํฉ๋๋ค.
- novalidate์์ฑ์ ์ถ๊ฐํ์ฌ ๋ธ๋ผ์ฐ์ ์ ๊ธฐ๋ณธ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ๋นํ์ฑํํฉ๋๋ค.
chat/templates/chat/room_detail.html ํ์ผ ๋ฎ์ด์ฐ๊ธฐยถ 1{% extends "chat/base.html" %}
 2
 3{% block content %}
 4<div class="flex flex-col h-[calc(100vh-16rem)]">
 5    <div class="bg-white rounded-lg shadow-md p-4 mb-4">
 6        <h1 class="text-2xl font-bold text-gray-800">{{ room.name }}</h1>
 7        <p class="text-sm text-gray-600">์์ฑ์ผ: {{ room.created_at|date:"Y-m-d H:i" }}</p>
 8    </div>
 9
10    <div id="messages-container">
11        <div id="chat-messages">
12            {% for message in message_list %}
13                <div>[{{ message.role }}] : {{ message.content }}</div>
14            {% endfor %}
15        </div>
16    </div>
17
18    <form method="post" action="{% url 'chat:message_new' room_pk=room.pk %}" novalidate>
19        {% csrf_token %}
20        <div class="flex gap-2">
21            <input type="text" name="content" required autocomplete="off" placeholder="๋ฉ์์ง๋ฅผ ์
๋ ฅํ์ธ์..."
22                autofocus class="flex-1 bg-gray-100 rounded-lg px-4 py-2">
23            <button type="submit"
24                class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition-colors duration-300">
25                ์ ์ก
26            </button>
27        </div>
28    </form>
29</div>
30{% endblock %}
์ด๋ ๊ฒ ๋ธ๋ผ์ฐ์  ๊ธฐ๋ณธ์ <form> ์ ์ก์ ํ์ฉํ์ฌ ์ฑํ
 ๋ฉ์์ง๋ฅผ ์ ์กํ  ์ ์์ต๋๋ค.
ํ์ผ ์
๋ก๋๊ฐ ํ์ํ  ๋์๋ <form> ํ๊ทธ์ enctype="multipart/form-data" ์์ฑ์ ์ถ๊ฐํ์๋ฉด ๋ธ๋ผ์ฐ์ ์์ ์์์ ํ์ผ ์ ์ก๊น์ง ํด์ค๋๋ค.
์๋ฐ์คํฌ๋ฆฝํธ๋ฅผ ์จ์ผ๋ง ๋ชจ๋ํ ์ ํ๋ฆฌ์ผ์ด์
์ด ๋๋ ๊ฒ์ ์๋๋๋ค.
์ฐธ๊ณ
room_detail.html ํ
ํ๋ฆฟ์์๋ ์ฑํ
๋ฉ์์ง ์
๋ ฅํผ ํ๋ ๋ ๋๋ง์ ์ฅ๊ณ  Form์ ํ์ฉํด์ ๊ตฌํํ  ์ ์์ต๋๋ค.
๋์ ํ๋ฉดยถ
์ ๋ด์ฉ์ ๋ชจ๋ ์ ์ฉํ๊ณ ์ฑํ ๋ฐฉ์์ ์ฑํ ๋ฉ์์ง๋ฅผ ์ ๋ ฅํ๊ณ ์ ์ ๊ธฐ๋ค๋ ค๋ณด์๋ฉด ์ด์ด์ ์ฑํ ์๋ต์ ๋ฐ์ผ์๊ฒ ๋ฉ๋๋ค.
 
ํ์ด์ง ์ ํ์ด ๋ฐ์ํ๋ ๋ฐ ๋๋ผ์
จ๋์? ์๋ ๋น ๋ฅด๊ฒ ํ์ด์ง๊ฐ ์ ํ๋์ด ๋๋ผ๊ธฐ ์ด๋ ค์ธ ์ ์์ต๋๋ค.
python manage.py runserver ๋ช
๋ น์ด๋ฅผ ์คํํ ์ฝ์ ์ถ๋ ฅ ๋ก๊ทธ๋ฅผ ๋ณด์๋ฉด ํ์ด์ง ์ ํ์ด ๋ฐ์ํ์์ ํ์ธํ์ค ์ ์์ต๋๋ค.
[28/Feb/2025 06:11:32] "POST /chat/1/messages/new/ HTTP/1.1" 302 0
[28/Feb/2025 06:11:32] "GET /chat/1/ HTTP/1.1" 200 16719
์๋ก์ด ์ฑํ
 ๋ฉ์์ง๋ฅผ /chat/1/messages/new/ ์ฃผ์๋ก POST ๋ฐฉ์์ผ๋ก ๋ณด๋ด๋ฉฐ ํ์ด์ง ์ ํ์ด ๋ฐ์ํ๊ณ ,
์๋ฒ์์ AI ์๋ต ์์ฑ ํ์ /chat/1/ ์ฃผ์๋ก ์ด๋ํ๋ผ๋ 302 ์๋ต์ ๋ณด๋๊ตฌ์.
์ด์ ๋ธ๋ผ์ฐ์ ๋ /chat/1/ ์ฃผ์๋ก ๋ค์ ์ด๋์ ํ ์ํฉ์
๋๋ค.