๐ ๋ฌธ์ ๊ฒ์ ํ์ด์งยถ
๋ณ๊ฒฝ ํ์ผ์ ํ ๋ฒ์ ๋ฎ์ด์ฐ๊ธฐ ํ์ค๋ ค๋ฉด, pyhub-git-commit-apply ์ ํธ๋ฆฌํฐ ์ค์นํ์ ํ์, ํ๋ก์ ํธ ๋ฃจํธ์์ ์๋ ๋ช ๋ น ์คํํ์๋ฉด ์ง์ ์ปค๋ฐ์ ๋ชจ๋ ํ์ผ์ ๋ค์ด๋ฐ์ ํ์ฌ ๊ฒฝ๋ก์ ๋ฎ์ด์ฐ๊ธฐํฉ๋๋ค.
python -m pyhub_git_commit_apply https://github.com/pyhub-kr/django-webchat-rag-langcon2025/commit/bc945b63147a319e92e04b26e58a6b468c4c4640
uv
๋ฅผ ์ฌ์ฉํ์ค ๊ฒฝ์ฐ
uv run pyhub-git-commit-apply https://github.com/pyhub-kr/django-webchat-rag-langcon2025/commit/bc945b63147a319e92e04b26e58a6b468c4c4640
์ธ๋ฒ ํด์๋ก ์ง๋ต ๋ด์ฉ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ์ผ๋ ์ ์ฌ ๋ฌธ์ ๊ฒ์์ ์ง์ํ๋ ํ์ด์ง๋ฅผ ๊ตฌํํด๋ด ์๋ค. ์ฅ๊ณ ๋ชจ๋ธ ๊ธฐ๋ฐ์ผ๋ก ๋ฌธ์๊ฐ ์ ์ฅ๋์ด์์ด ์์ฝ๊ฒ ์ ์ฌ ๋ฌธ์ ๊ฒ์์ ์ง์ํ ์ ์์ต๋๋ค.

๋ชจ๋ธ์ page_content_obj
์บ์ ์์ฑ ์ถ๊ฐยถ
๋ฌธ์ ๋ชจ๋ธ์ .page_content
์์ฑ์ ๋ฌธ์์ด ํ์
์ธ๋ฐ,
โ๋ฌธ์๋ฒํธโ, โ์ ๋ชฉโ, โ๋ฌธ์IDโ, โ๋ฒ๋ น๋ถ๋ฅโ, โ์์งโ, โํ์ โ, โํ์ผ๋ด์ฉโ, โ๊ณต๊ฐ์ฌ๋ถโ, โ๋ฌธ์๋ถ๋ฅโ ๋ฑ์ ์ ๋ณด๊ฐ JSON ํฌ๋งท์ผ๋ก ์ ์ฅ๋์ด์์ต๋๋ค.
๊ฐ Key ์ ๋ณด์ ์ ๊ทผํ๊ธฐ ์ํด์๋ JSON ์ญ์ง๋ ฌํ๊ฐ ํ์ํ๊ตฌ์. ๋งค Key์ ์ ๊ทผํ ๋๋ง๋ค ์ญ์ง๋ ฌํ๋ฅผ ํ๋ฉด ์ฑ๋ฅ์ด ๋จ์ด์ง๋ฏ๋ก,
page_content_obj
์บ์ ์์ฑ์ ์ถ๊ฐํ์ฌ ๊ฐ ์ธ์คํด์ค๋ง๋ค 1ํ๋ง ์ญ์ง๋ ฌํ๋ฅผ ์ํํ๊ณ , ์บ์ฑ๋ ๊ฐ์ฒด๋ฅผ ํ์ฉํ ๋ก ํฉ๋๋ค.
chat/models.py
ํ์ผ์ ๋ฎ์ด์ฐ๊ธฐยถ 1import json
2
3from django.utils.functional import cached_property
4from pyhub.rag.fields.sqlite import SQLiteVectorField
5from pyhub.rag.models.sqlite import SQLiteVectorDocument
6
7class TaxLawDocument(SQLiteVectorDocument):
8 embedding = SQLiteVectorField(
9 dimensions=3072,
10 editable=False,
11 embedding_model="text-embedding-3-large",
12 )
13
14 @cached_property
15 def page_content_obj(self):
16 return json.loads(self.page_content)
chat/models.py
ํ์ผ์ ๋ฎ์ด์ฐ๊ธฐยถ 1import json
2
3from django.utils.functional import cached_property
4from pyhub.rag.fields.postgres import PGVectorField
5from pyhub.rag.models.postgres import PGVectorDocument
6
7class TaxLawDocument(PGVectorDocument):
8 embedding = PGVectorField(
9 dimensions=3072,
10 editable=False,
11 embedding_model="text-embedding-3-large",
12 )
13
14 @cached_property
15 def page_content_obj(self):
16 return json.loads(self.page_content)
17
18 class Meta:
19 indexes = [
20 PGVectorDocument.make_hnsw_index(
21 "chat_taxlawdoc_idx",
22 "halfvec",
23 "cosine",
24 ),
25 ]
์ด์ ํ์ด์ฌ ์ฝ๋ ๋จ์์๋ doc.page_content_obj["์ ๋ชฉ"]
์ฒ๋ผ ์ ๊ทผํ ์ ์์ผ๋ฉฐ,
ํ
ํ๋ฆฟ ๋จ์์๋ {{ doc.page_content_obj.์ ๋ชฉ }}
์ฒ๋ผ ์ ๊ทผํ ์ ์์ต๋๋ค.
๋ทฐ ๊ตฌํยถ
๋ฆฌ์คํธ ๊ตฌํ์ ์ํด ListView
ํด๋์ค๋ฅผ ์์๋ฐ์ TaxLawDocumentListView
ํด๋์ค๋ฅผ ๊ตฌํํฉ๋๋ค.
model
์์ฑ๋ง TaxLawDocument
๋ชจ๋ธ๋ก ์ง์ ํ๋ฉด ํ ๋ฒ์ ์ ์ฒด ๋ฌธ์๊ฐ ์กฐํ๋๋ ์กฐํ ์ฑ๋ฅ์ด ๋จ์ด์ง๋๋ค.
paginate_by
์์ฑ์ ์ง์ ํ๋ฉด ListView
๋ฅผ ํตํด ์ฟผ๋ฆฌ์
๊ธฐ๋ฐ์์ ํ์ด์ง๋ค์ด์
์ ์ง์ํ์ง๋ง,
์ ์ฌ ๋ฌธ์ ๊ฒ์ ๊ฒฐ๊ณผ๋ ํ์ด์ง๋ค์ด์
์ ์ง์ํ์ง ์๊ธฐ์ paginate_by
์์ฑ์ ์ง์ ํ์ง ์๊ฒ ์ต๋๋ค.
๊ฒ์์ด query
์ธ์๊ฐ ์์ ๊ฒฝ์ฐ, ๋ชจ๋ธ ๋งค๋์ ์ similarity_search
๋ฉ์๋๋ฅผ ํตํด ์ ์ฌ ๋ฌธ์ ๊ฒ์์ ์ํํฉ๋๋ค.
ListView
์์๋ ์ฟผ๋ฆฌ์
์ ํตํด ๋ํดํธ ํ
ํ๋ฆฟ ์ด๋ฆ์ ์ฐพ๋๋ฐ์.
django-pyhub-rag
๋ผ์ด๋ธ๋ฌ๋ฆฌ์ sqlite-vec
๋ฐฑ์๋์ similarity_search
๋ฉ์๋๋ ์ฟผ๋ฆฌ์
์ด ์๋ ๋ฆฌ์คํธ๋ฅผ ๋ฐํํ๊ณ ,
get_queryset
๋ฉ์๋์์๋ ๋ฆฌ์คํธ๋ฅผ ๋ฐํํ๊ฒ ๋ฉ๋๋ค. ๊ทธ๋ผ ListView
ํด๋์ค์์ ๋ํดํธ ํ
ํ๋ฆฟ ์ด๋ฆ์ ๊ฒฐ์ ํ ์ ์๊ธฐ์,
template_name
์์ฑ์ ์ง์ ์ง์ ํด์ค๋๋ค.
chat/views.py
ํ์ผ์ ๋ฎ์ด์ฐ๊ธฐยถ 1from django.views.generic import ListView
2from .models import TaxLawDocument
3
4# ํ
ํ๋ฆฟ์์์ URL Reverse ์ฐธ์กฐ๋ฅผ ์ํด ๋น View ํจ์ ์ ์
5def room_list(request): pass
6def room_new(request): pass
7def room_detail(request, pk): pass
8
9# ๋ฌธ์ ๊ฒ์ ํ์ด์ง
10class TaxLawDocumentListView(ListView):
11 model = TaxLawDocument
12 # sqlite์ similarity_search ๋ฉ์๋๊ฐ ์ฟผ๋ฆฌ์
์ด ์๋ ๋ฆฌ์คํธ๋ฅผ ๋ฐํํ๊ธฐ ๋๋ฌธ์
13 # ListView์์ ํ
ํ๋ฆฟ ์ด๋ฆ์ ์ฐพ์ง ๋ชปํ๊ธฐ์ ์ง์ ์ง์ ํด์ค๋๋ค.
14 template_name = "chat/taxlawdocument_list.html"
15
16 def get_queryset(self):
17 qs = super().get_queryset()
18
19 query = self.request.GET.get("query", "").strip()
20 if query:
21 qs = qs.similarity_search(query) # noqa: list ํ์
22 else:
23 # ๊ฒ์์ด๊ฐ ์๋ค๋ฉด ๋น ์ฟผ๋ฆฌ์
์ ๋ฐํํฉ๋๋ค.
24 qs = qs.none()
25
26 return qs
URL ๋งคํ๋ ์ถ๊ฐํด์ฃผ์๊ตฌ์.
chat/urls.py
ํ์ผ์ ๋ฎ์ด์ฐ๊ธฐยถfrom django.urls import path
from . import views
app_name = "chat"
urlpatterns = [
path("", views.room_list, name="room_list"),
path("new/", views.room_new, name="room_new"),
path("<int:pk>/", views.room_detail, name="room_detail"),
path("docs/law/tax/", views.TaxLawDocumentListView.as_view()),
]
ํ ํ๋ฆฟ ๊ตฌํยถ
์๋ ๋ด์ฉ์ผ๋ก chat/templates/chat/base.html
๊ฒฝ๋ก์ ๋ถ๋ชจ ํ
ํ๋ฆฟ์ ์์ฑํฉ๋๋ค.
ํ์ฌ ๋ฌธ์ ๋ชฉ๋ก ํ์ด์ง ๋ฟ๋ง ์๋๋ผ, ์ดํ ์ฑํ
ํ์ด์ง์์๋ ์ฌ์ฉํ ๊ฒ์
๋๋ค.
tailwind css
: ์ ํธ๋ฆฌํฐ ๊ธฐ๋ฐ CSS ํ๋ ์์ํฌ๋ก ๋น ๋ฅธ UI ๊ตฌ์ฑ์ ์ํด ์ฌ์ฉhtmx
: ์๋ฒ์์ ๋น๋๊ธฐ ํต์ ์ ์๋ฐ์คํฌ๋ฆฝํธ์์ด HTML ์์ฑ์ผ๋ก ๊ฐํธํ๊ฒ ๊ตฌํํ๊ธฐ ์ํด ์ฌ์ฉalpine.js
: ์ํฏ๊ฐ์ ๋ฐ๋ฅธ UI ๋์์ ์๋ฐ์คํฌ๋ฆฝํธ ์์ด ๊ฐํธํ๊ฒ ๊ตฌํํ๊ธฐ ์ํด ์ฌ์ฉ
chat/templates/chat/base.html
ํ์ผ ์์ฑ
1<!doctype html>
2<html>
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>{% block title %}Django Chat{% endblock %}</title>
7 <script src="https://cdn.tailwindcss.com"></script>
8 <script src="https://unpkg.com/htmx.org"></script>
9 <script src="https://unpkg.com/alpinejs"></script>
10</head>
11<body class="bg-gray-100">
12 <div class="container mx-auto px-4 py-8">
13 <header class="mb-8">
14 <nav class="bg-white shadow-lg rounded-lg">
15 <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
16 <div class="flex justify-between h-16">
17 <div class="flex">
18 <div class="flex-shrink-0 flex items-center">
19 <a href="{% url 'chat:room_list' %}" class="text-xl font-bold text-gray-800">
20 Django Chat
21 </a>
22 </div>
23 </div>
24 <div class="flex items-center">
25 <a href="{% url 'chat:room_new' %}"
26 class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
27 ์ ์ฑํ
๋ฐฉ
28 </a>
29 </div>
30 </div>
31 </div>
32 </nav>
33 </header>
34
35 <main class="bg-white shadow-lg rounded-lg p-6">
36 {% block content %}
37 {% endblock %}
38 </main>
39
40 <footer class="mt-8 text-center text-gray-600 text-sm">
41 <p>© 2025 ํ์ด์ฌ์ฌ๋๋ฐฉ. All rights reserved.</p>
42 </footer>
43 </div>
44</body>
45</html>
chat/templates/chat/taxlawdocument_list.html
๊ฒฝ๋ก์ ์ ๋ถ๋ชจ ํ
ํ๋ฆฟ์ ์์๋ฐ์ ํ
ํ๋ฆฟ์ ์๋์ ๊ฐ์ด ์ ์ํฉ๋๋ค.
<form>
ํ๊ทธ๋ฅผ ํตํด ๊ฒ์ํผ์ ๊ตฌํํ์ต๋๋ค.<select>
ํ๊ทธ๋ฅผ ํตํด ๊ฒ์ ๋์์ ์ ํํ ์ ์๋๋ก ํ์ต๋๋ค.<input>
ํ๊ทธ๋ฅผ ํตํด ๊ฒ์์ด๋ฅผ ์ ๋ ฅํ ์ ์๋๋ก ํ์ต๋๋ค.<button>
ํ๊ทธ๋ฅผ ํตํด ๊ฒ์ ๋ฒํผ์ ๊ตฌํํ์ต๋๋ค.
{% if request.GET.query and not object_list %}
ํ ํ๋ฆฟ ํ๊ทธ๋ฅผ ํตํด ๊ฒ์๊ฒฐ๊ณผ๊ฐ ์์ ๊ฒฝ์ฐ ๊ฒ์๊ฒฐ๊ณผ๊ฐ ์์์ ์๋ฆฌ๋ ๋ฉ์์ง๋ฅผ ์ถ๋ ฅํฉ๋๋ค.{% for doc in object_list %}
ํ ํ๋ฆฟ ํ๊ทธ๋ฅผ ํตํด ๊ฒ์๊ฒฐ๊ณผ๋ฅผ ๋ฐ๋ณตํ์ฌ ์ถ๋ ฅํฉ๋๋ค.
chat/templates/chat/taxlawdocument_list.html
ํ์ผ ์์ฑ
chat/templates/chat/taxlawdocument_list.html
ํ์ผ ์์ฑยถ 1{% extends "chat/base.html" %}
2
3{% block content %}
4
5 <h2 class="text-2xl font-bold text-gray-800 mb-4">์ธ๋ฒ ํด์๋ก ์ง๋ต ๋ฌธ์</h2>
6
7 <div class="mb-6">
8 <form method="get" action="" class="flex items-center gap-2">
9 <div class="relative mr-2">
10 <select name="document_type"
11 class="px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
12 <option value="tax_qa">์ธ๋ฒ ํด์๋ก ์ง๋ต</option>
13 </select>
14 </div>
15 <div class="relative flex-grow">
16 <input type="text" name="query" placeholder="๊ฒ์์ด๋ฅผ ์
๋ ฅํ์ธ์" value="{{ request.GET.query|default:'' }}"
17 class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
18 </div>
19 <button type="submit"
20 class="px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
21 ๊ฒ์
22 </button>
23 </form>
24 </div>
25
26 {% if request.GET.query and not object_list %}
27 <div class="bg-yellow-50 border border-yellow-200 text-yellow-800 px-4 py-3 rounded-md mb-4">
28 ๊ฒ์๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค.
29 </div>
30 {% endif %}
31
32 {% if object_list %}
33 <div class="text-sm text-gray-600 mb-4 font-medium">
34 ์ด
35 <span class="font-semibold text-blue-600">{{ object_list|length }}</span>๊ฐ์ ๋ฌธ์๊ฐ ๊ฒ์๋์์ต๋๋ค.
36 </div>
37 {% endif %}
38
39 {% for doc in object_list %}
40 <div class="bg-white shadow-md rounded-lg p-6 mb-6 border border-gray-200">
41 <div class="mb-4">
42 <h3 class="text-lg font-semibold">
43 <span class="text-gray-500">[{{ doc.page_content_obj.๋ฌธ์๋ฒํธ|default:"๋ฌธ์๋ฒํธ ์์" }}]</span>
44
45 {{ doc.page_content_obj.์ ๋ชฉ|default:"์ ๋ชฉ ์์" }}
46
47 <small>
48 <a href="{{ doc.metadata.url }}" class="text-blue-600 hover:underline" target="_blank">
49 ์ถ์ฒ
50 </a>
51 </small>
52 </h3>
53 </div>
54
55 <div>
56 <table class="min-w-full divide-y divide-gray-200 mt-4">
57 <tbody class="bg-white divide-y divide-gray-200">
58 {% for key, value in doc.page_content_obj.items %}
59 {% if key != "๋ฌธ์๋ฒํธ" and key != "์ ๋ชฉ" and key != "์์ฑ์ผ์" and key != "์์ ์ผ์" %}
60 <tr class="{% cycle 'bg-gray-50' '' %}">
61 <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/4">
62 {{ key }}
63 </th>
64 <td class="px-6 py-4 text-sm text-gray-900">{{ value }}</td>
65 </tr>
66 {% endif %}
67 {% endfor %}
68 </tbody>
69 </table>
70 </div>
71 </div>
72 {% endfor %}
73
74{% endblock %}
์น๋ธ๋ผ์ฐ์ ๋ฅผ ์ด๊ณ http://localhost:8000/chat/docs/law/tax/ ํ์ด์ง์ ์ ์ํด์ฃผ์ธ์. ์๋์ ๊ฐ์ด ์ ์ฌ ๋ฌธ์ ๊ฒ์ ํ์ด์ง๋ฅผ ํ์ธํ์ค ์ ์์ต๋๋ค.. ์๋๋ โ์ฌํ ์์ถํ๋ ๊ฒฝ์ฐ ์์ธ์จ ์ฒจ๋ถ ์๋ฅ๋ก ์์ถ์ค์ ๋ช ์ธ์๊ฐ ์๋ ๊ฒฝ์ฐ ํด๊ฒฐ ๋ฐฉ๋ฒโ ๊ฒ์ ๊ฒฐ๊ณผ์ ๋๋ค.

๋ฌธ์ ์ ๋ชฉ ํด๋ฆญํ์ฌ, ๋ด์ฉ ์ ๊ณ ํด๊ธฐยถ
.similarity_search
๋ฉ์๋๋ ๋ํดํธ๋ก ์ต๋ 4๊ฐ์ ๋ฌธ์๋ฅผ ๋ฐํํฉ๋๋ค. ๊ทธ๋ฐ๋ฐ, ๊ฐ ๋ฌธ์์ ๋ด์ฉ์ด ๋๋ฌด ๊ธธ์ด์ ๋ชจ๋ฐ์ผ ํ๊ฒฝ์์๋ ๋ณด๊ธฐ ์ด๋ ค์ด๋ฐ์.
๋ฌธ์ ์ ๋ชฉ์ ํด๋ฆญํ๋ฉด ๋ด์ฉ์ ์ ๊ณ ํด๊ธฐ ํ ์ ์๋๋ก ๊ฐ์ ํด๋ณด๊ฒ ์ต๋๋ค.
๊ฒฝ๋ ์ํ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ alpine.js
์ฝ 45KB ์ฉ๋ ๋ฅผ ํ์ฉํ์ฌ ๋ฌธ์ ์ ๋ชฉ์ ํด๋ฆญํ๋ฉด ๋ด์ฉ์ ์ ๊ณ ํด๊ธฐ ํ ์ ์๋๋ก ํ
ํ๋ฆฟ์ ์์ ํฉ๋๋ค.
์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ถ๋ชจ ํ
ํ๋ฆฟ์์ ์ด๋ฏธ ํฌํจ๋์ด์์ต๋๋ค.
๊ฐ ๋ฌธ์๋ง๋ค
x-data="{ opened: false }"
์์ฑ์ ์ถ๊ฐํ์ฌ, ๋ฌธ์๋ง๋ค ๋ณ๋์ ์ํฏ๊ฐ์ ๊ฐ์ง๋๋ก ํฉ๋๋ค.๋ฌธ์ ๋ด์ฉ ์์๋
x-show="opened"
์์ฑ์ ์ถ๊ฐํ์ฌ,opened
์ํฏ๊ฐ์ด์ฐธ
์ผ ๋์๋ง ๋ณด์ด๋๋ก ํฉ๋๋ค.๋ฌธ์ ์ ๋ชฉ ์์๋
x-on:click="opened = ! opened"
์์ฑ์ ์ถ๊ฐํ์ฌ, ํด๋ฆญํ๋ฉดopened
์ํฏ๊ฐ์ ํ ๊ธํ๋๋ก ํฉ๋๋ค.
chat/templates/chat/taxlawdocument_list.html
ํ์ผ ๋ฎ์ด์ฐ๊ธฐ
1{% extends "chat/base.html" %}
2
3{% block content %}
4
5 <h2 class="text-2xl font-bold text-gray-800 mb-4">์ธ๋ฒ ํด์๋ก ์ง๋ต ๋ฌธ์</h2>
6
7 <div class="mb-6">
8 <form method="get" action="" class="flex items-center gap-2">
9 <div class="relative mr-2">
10 <select name="document_type"
11 class="px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
12 <option value="tax_qa">์ธ๋ฒ ํด์๋ก ์ง๋ต</option>
13 </select>
14 </div>
15 <div class="relative flex-grow">
16 <input type="text" name="query" placeholder="๊ฒ์์ด๋ฅผ ์
๋ ฅํ์ธ์" value="{{ request.GET.query|default:'' }}"
17 class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
18 </div>
19 <button type="submit"
20 class="px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
21 ๊ฒ์
22 </button>
23 </form>
24 </div>
25
26 {% if request.GET.query and not object_list %}
27 <div class="bg-yellow-50 border border-yellow-200 text-yellow-800 px-4 py-3 rounded-md mb-4">
28 ๊ฒ์๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค.
29 </div>
30 {% endif %}
31
32 {% if object_list %}
33 <div class="text-sm text-gray-600 mb-4 font-medium">
34 ์ด
35 <span class="font-semibold text-blue-600">{{ object_list|length }}</span>๊ฐ์ ๋ฌธ์๊ฐ ๊ฒ์๋์์ต๋๋ค.
36 </div>
37 {% endif %}
38
39 {% for doc in object_list %}
40 <div class="bg-white shadow-md rounded-lg p-6 mb-6 border border-gray-200"
41 x-data="{ opened: false }">
42 <div class="mb-4">
43 <h3 class="text-lg font-semibold">
44 <span class="text-gray-500">[{{ doc.page_content_obj.๋ฌธ์๋ฒํธ|default:"๋ฌธ์๋ฒํธ ์์" }}]</span>
45
46 <button x-on:click="opened = ! opened">
47 {{ doc.page_content_obj.์ ๋ชฉ|default:"์ ๋ชฉ ์์" }}
48 </button>
49
50 <small>
51 <a href="{{ doc.metadata.url }}" class="text-blue-600 hover:underline" target="_blank">
52 ์ถ์ฒ
53 </a>
54 </small>
55 </h3>
56 </div>
57
58 <div x-show="opened">
59 <table class="min-w-full divide-y divide-gray-200 mt-4">
60 <tbody class="bg-white divide-y divide-gray-200">
61 {% for key, value in doc.page_content_obj.items %}
62 {% if key != "๋ฌธ์๋ฒํธ" and key != "์ ๋ชฉ" and key != "์์ฑ์ผ์" and key != "์์ ์ผ์" %}
63 <tr class="{% cycle 'bg-gray-50' '' %}">
64 <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/4">
65 {{ key }}
66 </th>
67 <td class="px-6 py-4 text-sm text-gray-900">{{ value }}</td>
68 </tr>
69 {% endif %}
70 {% endfor %}
71 </tbody>
72 </table>
73 </div>
74 </div>
75 {% endfor %}
76
77{% endblock %}
