๐ฌ ์ฑํ ๋ฐฉ/๋ฉ์์ง ๋ชจ๋ธ ๋ฐ ๊ธฐ๋ณธ ํ์ด์ง ๊ตฌ์ฑยถ
๋ณ๊ฒฝ ํ์ผ์ ํ ๋ฒ์ ๋ฎ์ด์ฐ๊ธฐ ํ์ค๋ ค๋ฉด, pyhub-git-commit-apply ์ ํธ๋ฆฌํฐ ์ค์นํ์ ํ์, ํ๋ก์ ํธ ๋ฃจํธ์์ ์๋ ๋ช ๋ น ์คํํ์๋ฉด ์ง์ ์ปค๋ฐ์ ๋ชจ๋ ํ์ผ์ ๋ค์ด๋ฐ์ ํ์ฌ ๊ฒฝ๋ก์ ๋ฎ์ด์ฐ๊ธฐํฉ๋๋ค.
python -m pyhub_git_commit_apply https://github.com/pyhub-kr/django-webchat-rag-langcon2025/commit/190e9a11fd8fff79fb0b47475f81094f72b90729
uv
๋ฅผ ์ฌ์ฉํ์ค ๊ฒฝ์ฐ
uv run pyhub-git-commit-apply https://github.com/pyhub-kr/django-webchat-rag-langcon2025/commit/190e9a11fd8fff79fb0b47475f81094f72b90729
๋ชจ๋ธยถ
์ฑํ
๋ฐฉ์ ์ ์ฅํ ๋ชจ๋ธ๋ก์ Room
๋ชจ๋ธ์ ์ ์ํฉ๋๋ค.
.create_ai_message()
๋ฉ์๋๋ฅผ ํธ์ถํ๋ฉด ์ฑํ
๋ฐฉ์ ์ด์ ๋ฉ์์ง๋ค์ ์์งํ์ฌ AI ์๋ต์ ์์ฑํ๊ณ , ์ AI ๋ฉ์์ง๋ก์ ์ ์ฅํฉ๋๋ค.
์ฑํ
๋ฐฉ์ ์์คํ
ํ๋กฌํํธ๊ฐ ๋ณ๊ฒฝ๋๋ฉด ์ด์ ๋ฉ์์ง๋ค์ ๋ชจ๋ ์ญ์ ํ๋๋ก ํ์ต๋๋ค. ๋ฉ์์ง ์ญ์ ๋ ์ํฉ์ ๋ฐ๋ผ ์ ์ฉํ์ง ์์๋ ๋ฉ๋๋ค.
name
: ์ฑํ ๋ฐฉ ์ด๋ฆsystem_prompt
: ์ฑํ ๋ฐฉ ์์คํ ํ๋กฌํํธcreated_at
: ์ฑํ ๋ฐฉ ์์ฑ์ผ์updated_at
: ์ฑํ ๋ฐฉ ์์ ์ผ์
์ฑํ
๋ฉ์์ง๋ฅผ ์ ์ฅํ ๋ชจ๋ธ๋ก์ Message
๋ชจ๋ธ์ ์ ์ํฉ๋๋ค.
role
: ๋ฉ์์ง ์ญํ (user, assistant)content
: ๋ฉ์์ง ๋ด์ฉcreated_at
: ๋ฉ์์ง ์์ฑ์ผ์updated_at
: ๋ฉ์์ง ์์ ์ผ์room
: ์ฑํ ๋ฐฉ (Room ๋ชจ๋ธ ์ธ๋ํค)
erDiagram Room ||--o{ Message : has Room { string name string system_prompt datetime created_at datetime updated_at } Message { string role string content datetime created_at datetime updated_at string room_id }
chat/models.py
๋ฎ์ด์ฐ๊ธฐยถ 1import json
2
3from django.db import models
4from django.utils.functional import cached_property
5from django_lifecycle import LifecycleModelMixin, hook, AFTER_UPDATE
6from pyhub.rag.fields.sqlite import SQLiteVectorField
7from pyhub.rag.models.sqlite import SQLiteVectorDocument
8
9from chat.llm import LLM
10
11
12class TaxLawDocument(SQLiteVectorDocument):
13 embedding = SQLiteVectorField(
14 dimensions=3072,
15 editable=False,
16 embedding_model="text-embedding-3-large",
17 )
18
19 @cached_property
20 def page_content_obj(self):
21 return json.loads(self.page_content)
22
23
24class Room(LifecycleModelMixin, models.Model):
25 name = models.CharField(max_length=255)
26 system_prompt = models.TextField(blank=True)
27 created_at = models.DateTimeField(auto_now_add=True)
28 updated_at = models.DateTimeField(auto_now=True)
29
30 def __str__(self):
31 return self.name
32
33 @hook(AFTER_UPDATE, when="system_prompt", has_changed=True)
34 def on_after_update(self):
35 self.message_set.all().delete()
36
37 def create_ai_message(self):
38 # ํ์ฌ ๋ฐฉ์ ์ด์ ๋ฉ์์ง๋ค์ ์์ง
39 message_qs = self.message_set.all()
40 messages = [{"role": msg.role, "content": msg.content} for msg in message_qs]
41
42 # AI ์๋ต ์์ฑ
43 llm = LLM(
44 model="gpt-4o-mini",
45 temperature=1,
46 system_prompt=self.system_prompt,
47 initial_messages=messages,
48 )
49 ai_message = llm.make_reply()
50
51 # AI ์๋ต์ ์ ๋ฉ์์ง๋ก ์ ์ฅ
52 return self.message_set.create(
53 role=Message.Role.ASSISTANT,
54 content=ai_message,
55 )
56
57 class Meta:
58 ordering = ["-pk"]
59
60
61class Message(models.Model):
62 class Role(models.TextChoices):
63 USER = "user"
64 ASSISTANT = "assistant"
65
66 room = models.ForeignKey(Room, on_delete=models.CASCADE)
67 role = models.CharField(max_length=255, choices=Role.choices, default=Role.USER)
68 content = models.TextField()
69 created_at = models.DateTimeField(auto_now_add=True)
70 updated_at = models.DateTimeField(auto_now=True)
71
72 def __str__(self):
73 return self.content
74
75 class Meta:
76 ordering = ["pk"]
chat/models.py
๋ฎ์ด์ฐ๊ธฐยถ 1import json
2
3from django.db import models
4from django.utils.functional import cached_property
5from django_lifecycle import LifecycleModelMixin, hook, AFTER_UPDATE
6from pyhub.rag.fields.postgres import PGVectorField
7from pyhub.rag.models.postgres import PGVectorDocument
8
9from chat.llm import LLM
10
11
12class TaxLawDocument(PGVectorDocument):
13 embedding = PGVectorField(
14 dimensions=3072,
15 editable=False,
16 embedding_model="text-embedding-3-large",
17 )
18
19 @cached_property
20 def page_content_obj(self):
21 return json.loads(self.page_content)
22
23 class Meta:
24 indexes = [
25 PGVectorDocument.make_hnsw_index(
26 "chat_taxlawdoc_idx",
27 "halfvec",
28 "cosine",
29 ),
30 ]
31
32
33class Room(LifecycleModelMixin, models.Model):
34 name = models.CharField(max_length=255)
35 system_prompt = models.TextField(blank=True)
36 created_at = models.DateTimeField(auto_now_add=True)
37 updated_at = models.DateTimeField(auto_now=True)
38
39 def __str__(self):
40 return self.name
41
42 @hook(AFTER_UPDATE, when="system_prompt", has_changed=True)
43 def on_after_update(self):
44 self.message_set.all().delete()
45
46 def create_ai_message(self):
47 # ํ์ฌ ๋ฐฉ์ ์ด์ ๋ฉ์์ง๋ค์ ์์ง
48 message_qs = self.message_set.all()
49 messages = [{"role": msg.role, "content": msg.content} for msg in message_qs]
50
51 # AI ์๋ต ์์ฑ
52 llm = LLM(
53 model="gpt-4o-mini",
54 temperature=1,
55 system_prompt=self.system_prompt,
56 initial_messages=messages,
57 )
58 ai_message = llm.make_reply()
59
60 # AI ์๋ต์ ์ ๋ฉ์์ง๋ก ์ ์ฅ
61 return self.message_set.create(
62 role=Message.Role.ASSISTANT,
63 content=ai_message,
64 )
65
66 class Meta:
67 ordering = ["-pk"]
68
69
70class Message(models.Model):
71 class Role(models.TextChoices):
72 USER = "user"
73 ASSISTANT = "assistant"
74
75 room = models.ForeignKey(Room, on_delete=models.CASCADE)
76 role = models.CharField(max_length=255, choices=Role.choices, default=Role.USER)
77 content = models.TextField()
78 created_at = models.DateTimeField(auto_now_add=True)
79 updated_at = models.DateTimeField(auto_now=True)
80
81 def __str__(self):
82 return self.content
83
84 class Meta:
85 ordering = ["pk"]
์๋ก์ด ๋ชจ๋ธ์ ์ ์ํ์ผ๋, ๋ง์ด๊ทธ๋ ์ด์ ํ์ผ์ ์์ฑํ๊ณ (์์ ์ง์์ ์์ฑ), ๋ง์ด๊ทธ๋ ์ด์ ์ ํตํด ์ํ๋ SQL ๋ด์ญ์ ํ์ธํ๊ณ (์์ ์ง์์ ํ์ธ), ๋ง์ด๊ทธ๋ ์ด์ ์ ์ํํฉ๋๋ค (์์ ์ง์์ ์คํ).

ํผยถ
์ ์ ์๊ฒ ์ฑํ ๋ฐฉ๊ณผ ๋ฉ์์ง๋ฅผ ์ ๋ ฅํ ์ ์๋ ํผ์ ์ ๊ณตํ๊ณ , ์ ๋ ฅ๊ฐ์ ๋ํ ์ ํจ์ฑ ๊ฒ์ฆ์ ์ํด ๋ชจ๋ธํผ์ ์์ฑํฉ๋๋ค.
chat/forms.py
ํ์ผ ์์ฑยถ 1from django import forms
2from .models import Message, Room
3
4
5# ์ ์ฑํ
๋ฐฉ ์์ฑ ๋ฐ ์์ ํ์ด์ง์์
6# ์
๋ ฅ HTML ํผ ์์ฑ ๋ฐ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ๋ด๋น
7class RoomForm(forms.ModelForm):
8 class Meta:
9 model = Room
10 fields = ["name", "system_prompt"]
11
12
13# ์ฑํ
๋ฉ์์ง ์
๋ ฅ/์์ ํผ์ ์์ฑํ๊ณ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ๋ด๋น
14class MessageForm(forms.ModelForm):
15 class Meta:
16 model = Message
17 fields = ["content"]
๋ทฐยถ
3๊ฐ์ ํ์ด์ง๋ฅผ ๊ตฌํํฉ๋๋ค.
room_list
: ์ฑํ ๋ฐฉ ๋ชฉ๋ก ํ์ด์งmodel = Room
: ๋ชฉ๋ก์ ๊ตฌ์ฑํ ๋ชจ๋ธ
room_new
: ์ ์ฑํ ๋ฐฉ ์์ฑ ํ์ด์งform = RoomForm
: ์ ๋ ฅ๊ฐ์ ๋ํ ์ ํจ์ฑ ๊ฒ์ฆ์ ์ํํ๊ณ , ์ ๋ ฅํผ HTML์ ์์ฑํ ํผroom = Room
: ์ ๋ ฅ๊ฐ์ ์ ์ฅํ ๋ชจ๋ธsuccess_url = reverse_lazy("chat:room_list")
: ์ ๋ ฅ๊ฐ์ ์ ์ฅํ ํ ์ด๋ํ URL
room_detail
: ์ฑํ ๋ฐฉ ์ฑํ ํ์ด์ง์ง์
pk
์ ์ฑํ ๋ฐฉ์ ์กฐํํ๊ณ , ํด๋น ์ฑํ ๋ฐฉ ๋ด ๋ชจ๋ ๋ฉ์์ง๋ฅผ ์กฐํํฉ๋๋ค.
chat/views.py
ํ์ผ ๋ฎ์ด์ฐ๊ธฐยถ 1from django.shortcuts import get_object_or_404, render
2from django.urls import reverse_lazy
3from django.views.generic import ListView, CreateView
4
5from .forms import RoomForm
6from .models import Room, TaxLawDocument
7
8
9# ์ฑํ
๋ฐฉ ๋ชฉ๋ก ํ์ด์ง (ํด๋์ค ๊ธฐ๋ฐ ๋ทฐ)
10room_list = ListView.as_view(model=Room)
11
12
13# ์ ์ฑํ
๋ฐฉ ์์ฑ ํ์ด์ง (ํด๋์ค ๊ธฐ๋ฐ ๋ทฐ)
14room_new = CreateView.as_view(
15 model=Room,
16 form_class=RoomForm,
17 success_url=reverse_lazy("chat:room_list"),
18)
19
20
21# ์ฑํ
๋ฐฉ ์ฑํ
ํ์ด์ง (ํจ์ ๊ธฐ๋ฐ ๋ทฐ)
22def room_detail(request, pk):
23 # ์ง์ ์ฑํ
๋ฐฉ ์กฐํํ๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์์ผ๋ฉด 404 ์ค๋ฅ ๋ฐ์
24 room = get_object_or_404(Room, pk=pk)
25 # ์ง์ ์ฑํ
๋ฐฉ์ ๋ชจ๋ ๋ํ ๋ชฉ๋ก
26 message_list = room.message_set.all()
27 return render(
28 request,
29 "chat/room_detail.html",
30 {
31 "room": room,
32 "message_list": message_list,
33 },
34 )
35
36
37# ๋ฌธ์ ๊ฒ์ ํ์ด์ง
38class TaxLawDocumentListView(ListView):
39 model = TaxLawDocument
40 # sqlite์ similarity_search ๋ฉ์๋๊ฐ ์ฟผ๋ฆฌ์
์ด ์๋ ๋ฆฌ์คํธ๋ฅผ ๋ฐํํ๊ธฐ ๋๋ฌธ์
41 # ListView์์ ํ
ํ๋ฆฟ ์ด๋ฆ์ ์ฐพ์ง ๋ชปํ๊ธฐ์ ์ง์ ์ง์ ํด์ค๋๋ค.
42 template_name = "chat/taxlawdocument_list.html"
43
44 def get_queryset(self):
45 qs = super().get_queryset()
46
47 query = self.request.GET.get("query", "").strip()
48 if query:
49 qs = qs.similarity_search(query) # noqa: list ํ์
50 else:
51 # ๊ฒ์์ด๊ฐ ์๋ค๋ฉด ๋น ์ฟผ๋ฆฌ์
์ ๋ฐํํฉ๋๋ค.
52 qs = qs.none()
53
54 return qs
๊ฐ ๋ทฐ์ ๋ํด URL ํจํด๋ ์์ ๐ ๋ฌธ์ ๊ฒ์ ํ์ด์ง ๋ฌธ์์์ ์์ฑํ์์ต๋๋ค.
ํ ํ๋ฆฟยถ
chat/templates/chat/room_list.html
ํ์ผ ์์ฑยถ 1{% extends "chat/base.html" %}
2
3{% block content %}
4 <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
5 {% for room in room_list %}
6 <div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
7 <img src="https://placehold.co/600x400/e2e8f0/475569?text=Chat+Room" alt="์ฑํ
๋ฐฉ ์ด๋ฏธ์ง" class="w-full h-48 object-cover">
8 <div class="p-4">
9 <h2 class="text-xl font-semibold text-gray-800 mb-2">{{ room.name }}</h2>
10 <p class="text-gray-600 text-sm mb-4">์์ฑ์ผ: {{ room.created_at|date:"Y-m-d H:i" }}</p>
11 <a href="{% url 'chat:room_detail' room.pk %}"
12 class="inline-block w-full text-center bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 transition-colors duration-300">
13 ์
์ฅํ๊ธฐ
14 </a>
15 </div>
16 </div>
17 {% empty %}
18 <div class="col-span-full text-center py-8">
19 <p class="text-gray-500 text-lg">์์ฑ๋ ์ฑํ
๋ฐฉ์ด ์์ต๋๋ค.</p>
20 <a href="{% url 'chat:room_new' %}"
21 class="inline-block mt-4 bg-indigo-600 text-white py-2 px-6 rounded-md hover:bg-indigo-700 transition-colors duration-300">
22 ์ ์ฑํ
๋ฐฉ ๋ง๋ค๊ธฐ
23 </a>
24 </div>
25 {% endfor %}
26 </div>
27{% endblock %}
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 class="flex-1 bg-gray-50 rounded-lg shadow-inner p-4 mb-4 overflow-y-auto">
11 <div class="space-y-4" id="chat-messages">
12 {% for message in message_list %}
13 <div class="flex {% if message.is_ai %}justify-start{% else %}justify-end{% endif %}">
14 <div class="{% if message.is_ai %}bg-white{% else %}bg-indigo-600 text-white{% endif %} rounded-lg px-4 py-2 max-w-[80%] shadow">
15 <div class="text-sm {% if message.is_ai %}text-gray-600{% else %}text-indigo-100{% endif %} mb-1">
16 {{ message.is_ai|yesno:"AI,You" }}
17 </div>
18 <p class="break-words">{{ message.content }}</p>
19 <div class="text-xs {% if message.is_ai %}text-gray-400{% else %}text-indigo-200{% endif %} text-right mt-1">
20 {{ message.created_at|date:"H:i" }}
21 </div>
22 </div>
23 </div>
24 {% endfor %}
25 </div>
26 </div>
27
28 <form class="flex gap-2" method="post">
29 {% csrf_token %}
30 <input type="text" name="content" required autocomplete="off"
31 class="flex-1 rounded-lg border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
32 placeholder="๋ฉ์์ง๋ฅผ ์
๋ ฅํ์ธ์...">
33 <button type="submit"
34 class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition-colors duration-300">
35 ์ ์ก
36 </button>
37 </form>
38 </div>
39{% endblock %}
chat/templates/chat/room_form.html
ํ์ผ ์์ฑยถ 1{% extends "chat/base.html" %}
2
3{% block content %}
4<div class="max-w-2xl mx-auto">
5 <div class="bg-white rounded-lg shadow-md p-6">
6 <h1 class="text-2xl font-bold text-gray-800 mb-6">์ ์ฑํ
๋ฐฉ ๋ง๋ค๊ธฐ</h1>
7
8 <form method="post" novalidate>
9 {% csrf_token %}
10
11 <div class="mb-4">
12 <label for="{{ form.name.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">์ฑํ
๋ฐฉ
13 ์ด๋ฆ</label>
14 <input type="text" name="{{ form.name.name }}" id="{{ form.name.id_for_label }}" required
15 class="w-full rounded-lg border-0 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
16 {% if form.name.value %}value="{{ form.name.value }}" {% endif %}>
17 {% if form.name.errors %}
18 <p class="mt-2 text-sm text-red-600">{{ form.name.errors.0 }}</p>
19 {% endif %}
20 </div>
21
22 <div class="mb-4">
23 <label for="{{ form.system_prompt.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">์์คํ
24 ํ๋กฌํํธ</label>
25 <textarea name="{{ form.system_prompt.name }}" id="{{ form.system_prompt.id_for_label }}"
26 class="w-full rounded-lg border-0 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
27 {% if form.system_prompt.value %}value="{{ form.system_prompt.value }}" {% endif %}></textarea>
28 {% if form.system_prompt.errors %}
29 <p class="mt-2 text-sm text-red-600">{{ form.system_prompt.errors.0 }}</p>
30 {% endif %}
31 </div>
32
33 <div class="flex justify-end">
34 <button type="submit"
35 class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition-colors duration-300">
36 ์์ฑํ๊ธฐ
37 </button>
38 </div>
39 </form>
40 </div>
41</div>
42{% endblock %}
์น ํ์ด์ง ๋์ ํ์ธยถ
์๋ ๋ช ๋ น์ผ๋ก ์ฅ๊ณ ๊ฐ๋ฐ ์น์๋ฒ๋ฅผ ๊ตฌ๋ํฉ๋๋ค.
python manage.py runserver 0.0.0.0:8000
ํฌ๋กฌ์ด๋ ์ฃ์ง ๋ธ๋ผ์ฐ์ ๋ฅผ ํตํด http://localhost:8000 ์ฃผ์๋ก ์ ์ํด์ฃผ์ธ์.

๊ทธ๋ผ mysite/urls.py
์ ์ ์๋ RedirectView
์ ์ํด /chat/
์ฃผ์๋ก ์๋ ์ด๋ํฉ๋๋ค.
โ์ ์ฑํ
๋ฐฉ ๋ง๋ค๊ธฐโ ๋ฒํผ์ ํด๋ฆญํ์๋ฉด /chat/new/
์ฃผ์๋ก ์ด๋ํฉ๋๋ค.

http://localhost:8000/chat/new/ ์ฃผ์์์๋ ์๋ก์ด ์ฑํ ๋ฐฉ์ ์์ฑํ ์ ์๋ ํผ์ด ์ ๊ณต๋ฉ๋๋ค. โ์ฑํ ๋ฐฉ ์ด๋ฆโ๊ณผ ์ฑํ ๋ฐฉ์์ ์ฌ์ฉํ โ์์คํ ํ๋กฌํํธโ๋ฅผ ์ ๋ ฅํ๊ณ โ์์ฑํ๊ธฐโ ๋ฒํผ์ ํด๋ฆญํ๋ฉด ์ฑํ ๋ฐฉ์ด ์์ฑ๋ฉ๋๋ค.
๋ฒ์ญ, ์์คํ ํ๋กฌํํธ
๋๋ ๋ฒ์ญ๊ฐ์ผ.
ํ๊ตญ์ด๋ก ๋ฌผ์ด๋ณด๋ฉด ํ๊ตญ์ด๋ก ๋๋ตํ๋ฉฐ ์์ด ๋ฒ์ญ์ ํจ๊ป ์ ๊ณตํด์ฃผ๊ณ ,
์์ด๋ก ๋ฌผ์ด๋ณด๋ฉด ์์ด๋ก ๋๋ตํ์ฌ ํ๊ธ ๋ฒ์ญ์ ํจ๊ป ์ ๊ณตํด์ค.
์์:
<์ง๋ฌธ>์๋
ํ์ธ์.</์ง๋ฌธ>
<๋ต๋ณ>๋ฐ๊ฐ์ต๋๋ค. ์ ๋ Tom ์
๋๋ค. (์์ด: Nice to meet you. I am Tom.)</๋ต๋ณ>
<์ง๋ฌธ>Hello.</์ง๋ฌธ>
<๋ต๋ณ>Nice to meet you. I am Tom. (ํ๊ตญ์ด: ์๋
ํ์ธ์. ์ ๋ Tom ์
๋๋ค.)</๋ต๋ณ>

์ฐธ๊ณ
chat/templates/chat/room_form.html
ํ
ํ๋ฆฟ์์๋ ํผ ํ๋๋ง๋ค ์ผ์ผ์ด HTML ๋งํฌ์
์ ์์ฑํ์ง๋ง,
django-crispy-forms
๋ฑ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํด ํผ ํ๋์ ๋ํ ๋งํฌ์
์ ์๋์ผ๋ก ์์ฑํ ์ ์๊ณ ,
HTML ์ฝ๋๊ฐ ์๋ ํ์ด์ฌ ์ฝ๋๋ก ๋ ์ด์์์ ๊ตฌ์ฑํ๊ณ ์์ ฏ์ ์ง์ ํ ์ ์์ต๋๋ค.
์ฑํ ๋ฐฉ ์์ฑ ํ์ ์๋์ผ๋ก ์ฑํ ๋ฐฉ ๋ชฉ๋ก ํ์ด์ง๋ก ์ด๋ํ์ต๋๋ค. โ์ ์ฅํ๊ธฐโ ๋ฒํผ์ ํด๋ฆญํ์๋ฉด ์ฑํ ๋ฐฉ ์ฑํ ํ์ด์ง๋ก ์ด๋ํฉ๋๋ค.

http://localhost:8000/chat/1/ ์ฃผ์์์๋ ์ฑํ
๋ฐฉ ์ฑํ
ํ์ด์ง๊ฐ ์ ๊ณต๋ฉ๋๋ค.
์ฑํ
๋ฐฉ์ ๋ฐ๋ผ /chat/1/
, /chat/2/
, /chat/3/
๋ฑ ์ฃผ์๊ฐ ๋ฌ๋ผ์ง๋๋ค.
์์ง ์ฑํ
๋ฉ์์ง ์ ์ก ๋ฐ ์๋ต ๊ธฐ๋ฅ์ด ๊ตฌํ๋์ง ์์์ต๋๋ค.
