๐Ÿ“ HTML Form๊ณผ ์žฅ๊ณ  Form์„ ํ™œ์šฉํ•œ ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์ „์†ก ๋ฐ ์‘๋‹ตยถ

์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์ „์†ก ๋ทฐยถ

์ƒˆ๋กœ์šด ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†ก๋ฐ›์„ 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์„ ํ™œ์šฉํ•ด์„œ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋™์ž‘ ํ™”๋ฉดยถ

์œ„ ๋‚ด์šฉ์„ ๋ชจ๋‘ ์ ์šฉํ•˜๊ณ  ์ฑ„ํŒ…๋ฐฉ์—์„œ ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์ž ์‹œ ๊ธฐ๋‹ค๋ ค๋ณด์‹œ๋ฉด ์ด์–ด์„œ ์ฑ„ํŒ… ์‘๋‹ต์„ ๋ฐ›์œผ์‹œ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

../../_images/play.gif

ํŽ˜์ด์ง€ ์ „ํ™˜์ด ๋ฐœ์ƒํ–ˆ๋Š” ๋ฐ ๋Š๋ผ์…จ๋‚˜์š”? ์›Œ๋‚™ ๋น ๋ฅด๊ฒŒ ํŽ˜์ด์ง€๊ฐ€ ์ „ํ™˜๋˜์–ด ๋Š๋ผ๊ธฐ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 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/ ์ฃผ์†Œ๋กœ ๋‹ค์‹œ ์ด๋™์„ ํ•œ ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค.