๐ 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/
์ฃผ์๋ก ๋ค์ ์ด๋์ ํ ์ํฉ์
๋๋ค.