From 5a5407afccae1e90b6d9b8f5671baa94c7077f61 Mon Sep 17 00:00:00 2001 From: Sanghun95 Date: Mon, 2 Mar 2026 17:22:34 +0900 Subject: [PATCH 01/52] feat: integrate korean book metadata and UI citations --- backend/app/core/config.py | 2 +- backend/app/services/llm.py | 66 +- backend/data/books_mapping.json | 1010 ++++++++++++++++++++++ backend/scripts/generate_book_mapping.py | 204 +++++ backend/scripts/generate_sql_updates.py | 61 ++ backend/scripts/update_db_metadata.py | 137 +++ backend/update_metadata.sql | 409 +++++++++ frontend/components/chat/MessageList.tsx | 77 +- frontend/types/chat.ts | 3 + 9 files changed, 1931 insertions(+), 38 deletions(-) create mode 100644 backend/data/books_mapping.json create mode 100644 backend/scripts/generate_book_mapping.py create mode 100644 backend/scripts/generate_sql_updates.py create mode 100644 backend/scripts/update_db_metadata.py create mode 100644 backend/update_metadata.sql diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 691793f..00a2569 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -11,7 +11,7 @@ class Settings(BaseSettings): SUPABASE_SERVICE_KEY: str = "" # Use Service Role Key for backend operations model_config = SettingsConfigDict( - env_file=str(Path(__file__).resolve().parents[2] / ".env"), + env_file=str(Path(__file__).resolve().parents[3] / ".env"), env_file_encoding="utf-8" ) diff --git a/backend/app/services/llm.py b/backend/app/services/llm.py index b97af39..07b7c9d 100644 --- a/backend/app/services/llm.py +++ b/backend/app/services/llm.py @@ -1,4 +1,7 @@ +import os +import re import threading +from pathlib import Path import google.generativeai as genai from app.core.config import settings from langchain_core.prompts import PromptTemplate @@ -9,25 +12,66 @@ _llm = None _llm_lock = threading.Lock() +def get_all_gemini_keys() -> list[str]: + """Reads all active and commented GEMINI_API_KEYs from the root .env file.""" + keys = [] + env_path = Path(__file__).resolve().parents[3] / ".env" + + if env_path.exists(): + with open(env_path, "r", encoding="utf-8") as f: + content = f.read() + # Find all variations of GEMINI_API_KEY assignments + matches = re.findall(r'(?:#\s*)?GEMINI_API_KEY\s*=\s*(.+)', content) + for m in matches: + key = m.strip() + if key and key not in keys: + keys.append(key) + + # Ensure the one from environment variables/settings is also included + if getattr(settings, "GEMINI_API_KEY", None) and settings.GEMINI_API_KEY not in keys: + keys.insert(0, settings.GEMINI_API_KEY) + + return keys + def get_llm(): global _llm - if not settings.GEMINI_API_KEY: - raise RuntimeError("GEMINI_API_KEY must be configured") - if _llm is None: with _llm_lock: if _llm is None: # Double-checked locking - # Configure Gemini API natively (optional, if native SDK features are needed) - genai.configure(api_key=settings.GEMINI_API_KEY) + keys = get_all_gemini_keys() - # Configure LangChain model - # TODO: model gemini-2.5-flash will be deprecated by June 17, 2026. Plan migration to gemini-3-flash. - _llm = ChatGoogleGenerativeAI( - model="gemini-3-flash", - google_api_key=settings.GEMINI_API_KEY, + if not keys: + raise RuntimeError("No GEMINI_API_KEY found in .env or environment") + + # Configure Gemini API natively with the first key + genai.configure(api_key=keys[0]) + + print(f"Loaded {len(keys)} Gemini API keys for rotation/fallbacks.") + + # Create the primary model + primary_llm = ChatGoogleGenerativeAI( + model="gemini-2.5-flash-lite", + google_api_key=keys[0], temperature=0.7, - max_retries=2 + max_retries=1 ) + + if len(keys) > 1: + # Create fallback models with the other keys + fallback_llms = [ + ChatGoogleGenerativeAI( + model="gemini-2.5-flash-lite", + google_api_key=k, + temperature=0.7, + max_retries=1 + ) + for k in keys[1:] + ] + # LangChain will automatically retry with the next model if one throws an error (e.g. rate limit / quota) + _llm = primary_llm.with_fallbacks(fallback_llms) + else: + _llm = primary_llm + return _llm diff --git a/backend/data/books_mapping.json b/backend/data/books_mapping.json new file mode 100644 index 0000000..7bd9fc8 --- /dev/null +++ b/backend/data/books_mapping.json @@ -0,0 +1,1010 @@ +[ + { + "original_file": "A Budget of Paradoxes Volume I by Augustus De Morgan.txt", + "translated_title": "역설의 예산 1권", + "translated_author": "어거스터스 드 모르간", + "aladin_data": {} + }, + { + "original_file": "A Pickle for the Knowing Ones by Timothy Dexter.txt", + "translated_title": "아는 자들을 위한 곤경", + "translated_author": "티모시 덱스터", + "aladin_data": {} + }, + { + "original_file": "A Treatise of Human Nature by David Hume.txt", + "translated_title": "인간 본성론", + "translated_author": "데이비드 흄", + "aladin_data": { + "title": "인간이란 무엇인가 - 오성 정념 도덕 본성론", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=4359030&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/435/90/coversum/8949705206_1.jpg", + "author": "데이비드 흄 (지은이), 김성숙 (옮긴이)", + "isbn": "9788949705200" + } + }, + { + "original_file": "A Vindication of the Rights of Woman by Mary Wollstonecraft.txt", + "translated_title": "여권 옹호", + "translated_author": "메리 울스턴크래프트", + "aladin_data": { + "title": "여권의 옹호", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=45690064&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/4569/0/coversum/8994054596_1.jpg", + "author": "메리 울스턴크래프트 (지은이), 손영미 (옮긴이)", + "isbn": "9788994054599" + } + }, + { + "original_file": "Also sprach Zarathustra Ein Buch für Alle und Keinen German by Friedrich Wilhelm Nietzsche.txt", + "translated_title": "차라투스트라는 이렇게 말했다", + "translated_author": "프리드리히 니체", + "aladin_data": { + "title": "차라투스트라는 이렇게 말했다", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=454014&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/45/40/coversum/s352934786_1.jpg", + "author": "프리드리히 니체 (지은이), 장희창 (옮긴이)", + "isbn": "9788937460944" + } + }, + { + "original_file": "An Enquiry Concerning Human Understanding by David Hume.txt", + "translated_title": "인간 오성론", + "translated_author": "데이비드 흄", + "aladin_data": {} + }, + { + "original_file": "An Essay Concerning Humane Understanding Volume 1 by John Locke.txt", + "translated_title": "인간 오성론", + "translated_author": "존 로크", + "aladin_data": { + "title": "존 로크의 인간 오성론 읽기", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=90592125&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/9059/21/coversum/k092535101_1.jpg", + "author": "안병웅 (지은이)", + "isbn": "9791185136318" + } + }, + { + "original_file": "Apology by Plato.txt", + "translated_title": "소크라테스의 변명", + "translated_author": "플라톤", + "aladin_data": { + "title": "소크라테스의 변명 - 크리톤 파이돈 향연, 문예교양선서 30", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=224035&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/22/40/coversum/8931003714_3.jpg", + "author": "플라톤 (지은이), 황문수 (옮긴이)", + "isbn": "9788931003710" + } + }, + { + "original_file": "Apology Crito and Phaedo of Socrates by Plato.txt", + "translated_title": "소크라테스의 변론, 크리톤, 파이돈", + "translated_author": "플라톤", + "aladin_data": { + "title": "소크라테스의 변명·크리톤·파이돈·향연 (그리스어 원전 완역본) - 플라톤의 대화편", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=216792703&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/21679/27/coversum/k252636705_1.jpg", + "author": "플라톤 (지은이), 박문재 (옮긴이)", + "isbn": "9791190398039" + } + }, + { + "original_file": "As a man thinketh by James Allen.txt", + "translated_title": "생각하는 대로", + "translated_author": "제임스 앨런", + "aladin_data": { + "title": "제임스 앨런 원인과 결과의 법칙 - 사람은 생각하는 대로 살게 된다", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=345588057&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/34558/80/coversum/k732933167_1.jpg", + "author": "제임스 알렌 (지은이), 박선영 (옮긴이)", + "isbn": "9791171177660" + } + }, + { + "original_file": "Bacons Essays and Wisdom of the Ancients by Francis Bacon.txt", + "translated_title": "베이컨 수상록; 고대인의 지혜", + "translated_author": "프랜시스 베이컨", + "aladin_data": {} + }, + { + "original_file": "Beyond Good and Evil by Friedrich Wilhelm Nietzsche.txt", + "translated_title": "선악의 저편", + "translated_author": "프리드리히 니체", + "aladin_data": { + "title": "선악의 저편", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=174923171&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/17492/31/coversum/8957336117_1.jpg", + "author": "프리드리히 니체 (지은이), 박찬국 (옮긴이)", + "isbn": "9788957336113" + } + }, + { + "original_file": "Ciceros Tusculan Disputations by Marcus Tullius Cicero.txt", + "translated_title": "투스쿨룸 대화", + "translated_author": "마르쿠스 툴리우스 키케로", + "aladin_data": { + "title": "투스쿨룸 대화", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=286336783&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/28633/67/coversum/8957337679_1.jpg", + "author": "마르쿠스 툴리우스 키케로 (지은이), 김남우 (옮긴이)", + "isbn": "9788957337677" + } + }, + { + "original_file": "De Officiis Latin by Marcus Tullius Cicero.txt", + "translated_title": "의무론", + "translated_author": "마르쿠스 툴리우스 키케로", + "aladin_data": { + "title": "키케로의 의무론 - 그의 아들에게 보낸 편지", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=852420&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/85/24/coversum/8930606245_1.jpg", + "author": "마르쿠스 툴리우스 키케로 (지은이), 허승일 (옮긴이)", + "isbn": "9788930606240" + } + }, + { + "original_file": "Democracy and Education An Introduction to the Philosophy of Education by John Dewey.txt", + "translated_title": "민주주의와 교육", + "translated_author": "존 듀이", + "aladin_data": { + "title": "민주주의와 교육", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=922961&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/92/29/coversum/8925400669_2.jpg", + "author": "존 듀이 (지은이), 이홍우 (옮긴이)", + "isbn": "9788925400662" + } + }, + { + "original_file": "Democracy in America Volume 2 by Alexis de Tocqueville.txt", + "translated_title": "미국의 민주주의 2권", + "translated_author": "알렉시 드 토크빌", + "aladin_data": {} + }, + { + "original_file": "Demonology and Devil-lore by Moncure Daniel Conway.txt", + "translated_title": "악마학 및 악마 전승", + "translated_author": "몽큐어 대니얼 콘웨이", + "aladin_data": {} + }, + { + "original_file": "Discourse on the Method of Rightly Conducting Ones Reason and of Seeking Truth in the Sciences by René Descartes.txt", + "translated_title": "방법서설", + "translated_author": "르네 데카르트", + "aladin_data": { + "title": "방법서설 - 이성을 잘 인도하고 학문에서 진리를 찾기 위한", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=347983217&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/34798/32/coversum/k152933225_1.jpg", + "author": "르네 데카르트 (지은이), 이재훈 (옮긴이)", + "isbn": "9791170872443" + } + }, + { + "original_file": "Ecce Homo by Friedrich Wilhelm Nietzsche.txt", + "translated_title": "이 사람을 보라", + "translated_author": "프리드리히 니체", + "aladin_data": { + "title": "이 사람을 보라", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=302356844&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/30235/68/coversum/8957338195_1.jpg", + "author": "프리드리히 니체 (지은이), 박찬국 (옮긴이)", + "isbn": "9788957338193" + } + }, + { + "original_file": "Essays by Ralph Waldo Emerson by Ralph Waldo Emerson.txt", + "translated_title": "에머슨 수상록", + "translated_author": "랠프 월도 에머슨", + "aladin_data": { + "title": "에머슨 수상록", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=100408&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/10/4/coversum/8972432954_2.jpg", + "author": "랄프 왈도 에머슨 (지은이)", + "isbn": "9788972432951" + } + }, + { + "original_file": "Essays of Schopenhauer by Arthur Schopenhauer.txt", + "translated_title": "인생론", + "translated_author": "아르투어 쇼펜하우어", + "aladin_data": { + "title": "쇼펜하우어의 행복론과 인생론", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=308665960&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/30866/59/coversum/8932440093_1.jpg", + "author": "아르투어 쇼펜하우어 (지은이), 홍성광 (옮긴이)", + "isbn": "9788932440095" + } + }, + { + "original_file": "Ethics by Benedictus de Spinoza.txt", + "translated_title": "에티카", + "translated_author": "베네딕투스 데 스피노자", + "aladin_data": { + "title": "에티카 - 개정판", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=993377&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/99/33/coversum/8930625460_2.jpg", + "author": "베네딕투스 데 스피노자 (지은이), 강영계 (옮긴이)", + "isbn": "9788930625463" + } + }, + { + "original_file": "Etiquette by Emily Post.txt", + "translated_title": "에밀리 포스트의 에티켓", + "translated_author": "에밀리 포스트", + "aladin_data": {} + }, + { + "original_file": "Euthyphro by Plato.txt", + "translated_title": "에우티프론", + "translated_author": "플라톤", + "aladin_data": { + "title": "에우튀프론", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=272171162&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/27217/11/coversum/8957337342_1.jpg", + "author": "플라톤 (지은이), 강성훈 (옮긴이)", + "isbn": "9788957337349" + } + }, + { + "original_file": "Fundamental Principles of the Metaphysic of Morals by Immanuel Kant.txt", + "translated_title": "윤리 형이상학 정초", + "translated_author": "임마누엘 칸트", + "aladin_data": { + "title": "윤리형이상학 정초 - 개정2판", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=168355651&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/16835/56/coversum/8957336036_1.jpg", + "author": "임마누엘 칸트 (지은이), 백종현 (옮긴이)", + "isbn": "9788957336038" + } + }, + { + "original_file": "Goethes Theory of Colours by Johann Wolfgang von Goethe.txt", + "translated_title": "괴테의 색채론", + "translated_author": "요한 볼프강 폰 괴테", + "aladin_data": {} + }, + { + "original_file": "Gorgias by Plato.txt", + "translated_title": "고르기아스", + "translated_author": "플라톤", + "aladin_data": { + "title": "고르기아스", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=265344583&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/26534/45/coversum/8957337210_1.jpg", + "author": "플라톤 (지은이), 김인곤 (옮긴이)", + "isbn": "9788957337219" + } + }, + { + "original_file": "How We Think by John Dewey.txt", + "translated_title": "우리는 어떻게 생각하는가", + "translated_author": "존 듀이", + "aladin_data": {} + }, + { + "original_file": "Human All Too Human A Book for Free Spirits by Friedrich Wilhelm Nietzsche.txt", + "translated_title": "인간적인, 너무나 인간적인", + "translated_author": "프리드리히 니체", + "aladin_data": { + "title": "인간적인 너무나 인간적인 1", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=279599&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/27/95/coversum/8970132619_3.jpg", + "author": "프리드리히 니체 (지은이), 김미기 (옮긴이)", + "isbn": "9788970132617" + } + }, + { + "original_file": "Isis unveiled Volume 1 of 2 Science A master-key to mysteries of ancient and modern science and theology by H P Blavatsky.txt", + "translated_title": "베일 벗은 이시스", + "translated_author": "헬레나 블라바츠키", + "aladin_data": {} + }, + { + "original_file": "Laws by Plato.txt", + "translated_title": "법률", + "translated_author": "플라톤", + "aladin_data": { + "title": "플라톤의 법률", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=4646467&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/464/64/coversum/8930606296_1.jpg", + "author": "플라톤 (지은이), 박종현 (옮긴이)", + "isbn": "9788930606295" + } + }, + { + "original_file": "Leviathan by Thomas Hobbes.txt", + "translated_title": "리바이어던", + "translated_author": "토마스 홉스", + "aladin_data": { + "title": "리바이어던 1 - 교회국가 및 시민국가의 재료와 형태 및 권력", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=2480851&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/248/8/coversum/s392037901_2.jpg", + "author": "토마스 홉스 (지은이), 진석용 (옮긴이)", + "isbn": "9788930083379" + } + }, + { + "original_file": "Meditations by Emperor of Rome Marcus Aurelius.txt", + "translated_title": "명상록", + "translated_author": "마르쿠스 아우렐리우스", + "aladin_data": { + "title": "명상록 - 삶과 죽음을 고뇌한 어느 철학자 황제의 가장 사적인 기록", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=384592083&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/38459/20/coversum/k062135812_1.jpg", + "author": "마르쿠스 아우렐리우스 (지은이), 정미화 (옮긴이), 그레고리 헤이스 (해제)", + "isbn": "9791168273993" + } + }, + { + "original_file": "Nature by Ralph Waldo Emerson.txt", + "translated_title": "자연", + "translated_author": "랄프 왈도 에머슨", + "aladin_data": { + "title": "랄프 왈도 에머슨 : 자연", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=39251790&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/3925/17/coversum/8956607648_1.jpg", + "author": "랄프 왈도 에머슨 (지은이), 서동석 (옮긴이)", + "isbn": "9788956607641" + } + }, + { + "original_file": "On Heroes Hero-Worship and the Heroic in History by Thomas Carlyle.txt", + "translated_title": "영웅숭배론", + "translated_author": "토마스 칼라일", + "aladin_data": { + "title": "영웅숭배론", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=313531822&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/31353/18/coversum/8935678147_1.jpg", + "author": "토머스 칼라일 (지은이), 박상익 (옮긴이)", + "isbn": "9788935678143" + } + }, + { + "original_file": "On Liberty by John Stuart Mill.txt", + "translated_title": "자유론", + "translated_author": "존 스튜어트 밀", + "aladin_data": { + "title": "초판본 자유론 - 1859년 오리지널 초판본 표지 디자인", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=381938135&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/38193/81/coversum/k302034718_1.jpg", + "author": "존 스튜어트 밀 (지은이), 김희상 (옮긴이)", + "isbn": "9791175241961" + } + }, + { + "original_file": "On the Duty of Civil Disobedience by Henry David Thoreau.txt", + "translated_title": "시민 불복종", + "translated_author": "헨리 데이비드 소로", + "aladin_data": { + "title": "월든·시민 불복종 (합본 완역본)", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=284194464&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/28419/44/coversum/k742835213_1.jpg", + "author": "헨리 데이비드 소로 (지은이), 이종인 (옮긴이), 허버트 웬델 글리슨 (사진)", + "isbn": "9791139700503" + } + }, + { + "original_file": "On the Nature of Things by Titus Lucretius Carus.txt", + "translated_title": "사물의 본성에 관하여", + "translated_author": "루크레티우스", + "aladin_data": { + "title": "사물의 본성에 관하여", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=14599483&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/1459/94/coversum/8957332227_1.jpg", + "author": "루크레티우스 (지은이), 강대진 (옮긴이)", + "isbn": "9788957332221" + } + }, + { + "original_file": "On War by Carl von Clausewitz.txt", + "translated_title": "전쟁론", + "translated_author": "카를 폰 클라우제비츠", + "aladin_data": { + "title": "전쟁론 - 전면완역개정판", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=86524117&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/8652/41/coversum/8961951424_1.jpg", + "author": "카알 폰 클라우제비츠 (지은이), 김만수 (옮긴이)", + "isbn": "9788961951425" + } + }, + { + "original_file": "Pascals Pensées by Blaise Pascal.txt", + "translated_title": "파스칼의 팡세", + "translated_author": "블레즈 파스칼", + "aladin_data": { + "title": "파스칼의 팡세", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=367576319&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/36757/63/coversum/k952030294_1.jpg", + "author": "블레즈 파스칼 (지은이), 강현규 (엮은이), 이선미 (옮긴이)", + "isbn": "9791160029529" + } + }, + { + "original_file": "Perpetual Peace A Philosophical Essay by Immanuel Kant.txt", + "translated_title": "영구 평화론", + "translated_author": "임마누엘 칸트", + "aladin_data": { + "title": "영구 평화론 - 하나의 철학적 기획, 개정판", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=2881780&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/288/17/coversum/8930610439_1.jpg", + "author": "임마누엘 칸트 (지은이), 이한구 (옮긴이)", + "isbn": "9788930610438" + } + }, + { + "original_file": "Phaedo by Plato.txt", + "translated_title": "파이돈", + "translated_author": "플라톤", + "aladin_data": { + "title": "소크라테스의 변명·크리톤·파이돈·향연 (그리스어 원전 완역본) - 플라톤의 대화편", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=216792703&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/21679/27/coversum/k252636705_1.jpg", + "author": "플라톤 (지은이), 박문재 (옮긴이)", + "isbn": "9791190398039" + } + }, + { + "original_file": "Phaedrus by Plato.txt", + "translated_title": "파이드로스", + "translated_author": "플라톤", + "aladin_data": { + "title": "파이드로스 - 그리스어 원전 번역판", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=1820615&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/182/6/coversum/8931005881_2.jpg", + "author": "플라톤 (지은이), 조대호 (옮긴이)", + "isbn": "9788931005882" + } + }, + { + "original_file": "Plato and the Other Companions of Sokrates 3rd ed Volume 1 by George Grote.txt", + "translated_title": "플라톤과 소크라테스의 동반자들", + "translated_author": "조지 그로트", + "aladin_data": {} + }, + { + "original_file": "Plutarchs Morals by Plutarch.txt", + "translated_title": "플루타르코스 영웅전", + "translated_author": "플루타르코스", + "aladin_data": { + "title": "플루타르코스 영웅전", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=6970308&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/697/3/coversum/8991290337_2.jpg", + "author": "플루타르코스 (지은이), 천병희 (옮긴이)", + "isbn": "9788991290334" + } + }, + { + "original_file": "Politics A Treatise on Government by Aristotle.txt", + "translated_title": "정치학", + "translated_author": "아리스토텔레스", + "aladin_data": { + "title": "정치학", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=4399813&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/439/98/coversum/8991290280_1.jpg", + "author": "아리스토텔레스 (지은이), 천병희 (옮긴이)", + "isbn": "9788991290280" + } + }, + { + "original_file": "Pragmatism A New Name for Some Old Ways of Thinking by William James.txt", + "translated_title": "실용주의: 어떤 오래된 사고방식에 대한 새로운 이름", + "translated_author": "윌리엄 제임스", + "aladin_data": {} + }, + { + "original_file": "Psyche The Cult of Souls and Belief in Immortality among the Greeks by Erwin Rohde.txt", + "translated_title": "그리스 정신사: 영혼 숭배와 불멸에 대한 신념", + "translated_author": "에르빈 로데", + "aladin_data": {} + }, + { + "original_file": "Psychology of the Unconscious by C G Jung.txt", + "translated_title": "무의식의 심리학", + "translated_author": "카를 구스타프 융", + "aladin_data": { + "title": "칼 융 무의식의 심리학", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=300205010&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/30020/50/coversum/k222838355_1.jpg", + "author": "칼 구스타프 융 (지은이), 정명진 (옮긴이)", + "isbn": "9791159201479" + } + }, + { + "original_file": "Reflections or Sentences and Moral Maxims by François duc de La Rochefoucauld.txt", + "translated_title": "잠언과 도덕적 격언", + "translated_author": "라 로슈푸코 공작", + "aladin_data": {} + }, + { + "original_file": "Revelations of Divine Love by of Norwich Julian.txt", + "translated_title": "신의 사랑에 대한 계시", + "translated_author": "노리치의 줄리안", + "aladin_data": {} + }, + { + "original_file": "Roman Stoicism by Edward Vernon Arnold.txt", + "translated_title": "로마 스토아주의", + "translated_author": "에드워드 버논 아놀드", + "aladin_data": {} + }, + { + "original_file": "Second Treatise of Government by John Locke.txt", + "translated_title": "통치론", + "translated_author": "존 로크", + "aladin_data": { + "title": "통치론 - 시민정부의 참된 기원, 범위 및 그 목적에 관한 시론", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=301106377&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/30110/63/coversum/897291780x_1.jpg", + "author": "존 로크 (지은이), 강정인, 문지영 (옮긴이)", + "isbn": "9788972917809" + } + }, + { + "original_file": "Siddhartha by Hermann Hesse.txt", + "translated_title": "싯다르타", + "translated_author": "헤르만 헤세", + "aladin_data": { + "title": "싯다르타", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=329596&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/32/95/coversum/s062934786_1.jpg", + "author": "헤르만 헤세 (지은이), 박병덕 (옮긴이)", + "isbn": "9788937460586" + } + }, + { + "original_file": "Sun Tzŭ on the Art of War The Oldest Military Treatise in the World by active 6th century BC Sunzi.txt", + "translated_title": "손자병법", + "translated_author": "손무", + "aladin_data": { + "title": "손자병법 - 이겨놓고 싸우는 인생의 지혜", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=372980631&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/37298/6/coversum/k292031545_1.jpg", + "author": "손무 (지은이), 소준섭 (옮긴이)", + "isbn": "9791139728002" + } + }, + { + "original_file": "Symposium by Plato.txt", + "translated_title": "향연", + "translated_author": "플라톤", + "aladin_data": { + "title": "소크라테스의 변명·크리톤·파이돈·향연 (그리스어 원전 완역본) - 플라톤의 대화편", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=216792703&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/21679/27/coversum/k252636705_1.jpg", + "author": "플라톤 (지은이), 박문재 (옮긴이)", + "isbn": "9791190398039" + } + }, + { + "original_file": "The Anatomy of Melancholy by Robert Burton.txt", + "translated_title": "우울의 해부", + "translated_author": "로버트 버튼", + "aladin_data": {} + }, + { + "original_file": "The Antichrist by Friedrich Wilhelm Nietzsche.txt", + "translated_title": "안티크리스트", + "translated_author": "프리드리히 빌헬름 니체", + "aladin_data": { + "title": "안티크리스트", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=35425033&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/3542/50/coversum/8957333444_1.jpg", + "author": "프리드리히 니체 (지은이), 박찬국 (옮긴이)", + "isbn": "9788957333440" + } + }, + { + "original_file": "The Birth of Tragedy or Hellenism and Pessimism by Friedrich Wilhelm Nietzsche.txt", + "translated_title": "비극의 탄생", + "translated_author": "프리드리히 빌헬름 니체", + "aladin_data": { + "title": "비극의 탄생", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=988511&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/98/85/coversum/8957331077_1.jpg", + "author": "프리드리히 니체 (지은이), 박찬국 (옮긴이)", + "isbn": "9788957331071" + } + }, + { + "original_file": "The Case of Wagner Nietzsche Contra Wagner and Selected Aphorisms by Friedrich Wilhelm Nietzsche.txt", + "translated_title": "우상의 황혼", + "translated_author": "프리드리히 니체", + "aladin_data": { + "title": "우상의 황혼", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=64193963&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/6419/39/coversum/8957334513_1.jpg", + "author": "프리드리히 니체 (지은이), 박찬국 (옮긴이)", + "isbn": "9788957334515" + } + }, + { + "original_file": "The City of God Volume I by Saint of Hippo Augustine.txt", + "translated_title": "신의 도시 1", + "translated_author": "히포의 아우구스티누스", + "aladin_data": {} + }, + { + "original_file": "The City of God Volume II by Saint of Hippo Augustine.txt", + "translated_title": "신의 도성 2", + "translated_author": "히포의 아우구스티누스", + "aladin_data": {} + }, + { + "original_file": "The Communist Manifesto by Karl Marx and Friedrich Engels.txt", + "translated_title": "공산당 선언", + "translated_author": "카를 마르크스, 프리드리히 엥겔스", + "aladin_data": { + "title": "공산당 선언", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=143257420&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/14325/74/coversum/k172532941_1.jpg", + "author": "카를 마르크스, 프리드리히 엥겔스 (지은이), 심철민 (옮긴이)", + "isbn": "9791187036548" + } + }, + { + "original_file": "The Confessions of St Augustine by Saint of Hippo Augustine.txt", + "translated_title": "고백록", + "translated_author": "히포의 아우구스티누스", + "aladin_data": {} + }, + { + "original_file": "The Consolation of Philosophy by Boethius.txt", + "translated_title": "철학의 위안", + "translated_author": "보에티우스", + "aladin_data": { + "title": "철학의 위안 - 완역본", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=147121964&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/14712/19/coversum/k002532053_2.jpg", + "author": "아니키우스 보이티우스 (지은이), 박문재 (옮긴이)", + "isbn": "9791187142430" + } + }, + { + "original_file": "The Critique of Pure Reason by Immanuel Kant.txt", + "translated_title": "순수이성비판", + "translated_author": "이마누엘 칸트", + "aladin_data": { + "title": "순수이성비판 1", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=669748&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/66/97/coversum/8957330836_1.jpg", + "author": "임마누엘 칸트 (지은이), 백종현 (옮긴이)", + "isbn": "9788957330838" + } + }, + { + "original_file": "The Enchiridion by Epictetus.txt", + "translated_title": "편람", + "translated_author": "에픽테토스", + "aladin_data": { + "title": "[POD] 에픽테토스 편람 (스토아 사상 철학자) : The Enchiridion (영어원서)", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=231320657&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/23132/6/coversum/k022637708_1.jpg", + "author": "에픽테토스 (지은이)", + "isbn": "9791127295479" + } + }, + { + "original_file": "The Essays of Arthur Schopenhauer Studies in Pessimism by Arthur Schopenhauer.txt", + "translated_title": "쇼펜하우어 철학 에세이: 비관주의 연구", + "translated_author": "아르투어 쇼펜하우어", + "aladin_data": {} + }, + { + "original_file": "The Essays of Arthur Schopenhauer the Wisdom of Life by Arthur Schopenhauer.txt", + "translated_title": "행복론", + "translated_author": "아르투어 쇼펜하우어", + "aladin_data": { + "title": "쇼펜하우어의 행복론과 인생론", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=308665960&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/30866/59/coversum/8932440093_1.jpg", + "author": "아르투어 쇼펜하우어 (지은이), 홍성광 (옮긴이)", + "isbn": "9788932440095" + } + }, + { + "original_file": "The Ethics of Aristotle by Aristotle.txt", + "translated_title": "니코마코스 윤리학", + "translated_author": "아리스토텔레스", + "aladin_data": { + "title": "니코마코스 윤리학 - 그리스어 원전 번역, 개정판", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=31685631&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/3168/56/coversum/8991290523_3.jpg", + "author": "아리스토텔레스 (지은이), 천병희 (옮긴이)", + "isbn": "9788991290525" + } + }, + { + "original_file": "The Genealogy of Morals by Friedrich Wilhelm Nietzsche.txt", + "translated_title": "도덕의 계보", + "translated_author": "프리드리히 니체", + "aladin_data": { + "title": "도덕의 계보", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=274647853&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/27464/78/coversum/8957337350_1.jpg", + "author": "프리드리히 니체 (지은이), 박찬국 (옮긴이)", + "isbn": "9788957337356" + } + }, + { + "original_file": "The Grand Inquisitor by Fyodor Dostoyevsky.txt", + "translated_title": "대심문관", + "translated_author": "표도르 도스토옙스키", + "aladin_data": {} + }, + { + "original_file": "The history of magic including a clear and precise exposition of its procedure its rites and its mysteries by Éliphas Lévi.txt", + "translated_title": "마법의 역사", + "translated_author": "엘리파스 레비", + "aladin_data": {} + }, + { + "original_file": "The Kama Sutra of Vatsyayana by Vatsyayana.txt", + "translated_title": "카마수트라", + "translated_author": "밧사이아나", + "aladin_data": {} + }, + { + "original_file": "The Lives and Opinions of Eminent Philosophers by Diogenes Laertius.txt", + "translated_title": "위대한 철학자들의 생애와 사상", + "translated_author": "디오게네스 라에르티오스", + "aladin_data": {} + }, + { + "original_file": "The Man Who Was Thursday A Nightmare by G K Chesterton.txt", + "translated_title": "목요일의 사나이", + "translated_author": "G. K. 체스터턴", + "aladin_data": {} + }, + { + "original_file": "The Marriage of Heaven and Hell by William Blake.txt", + "translated_title": "천국과 지옥의 결혼", + "translated_author": "윌리엄 블레이크", + "aladin_data": { + "title": "천국과 지옥의 결혼", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=141182&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/14/11/coversum/8937418460_2.jpg", + "author": "윌리엄 블레이크 (지은이), 김종철 (옮긴이)", + "isbn": "9788937418464" + } + }, + { + "original_file": "The Meditations of the Emperor Marcus Aurelius Antoninus by Emperor of Rome Marcus Aurelius.txt", + "translated_title": "마르쿠스 아우렐리우스 황제의 명상록", + "translated_author": "마르쿠스 아우렐리우스", + "aladin_data": { + "title": "명상록 - 삶과 죽음을 고뇌한 어느 철학자 황제의 가장 사적인 기록", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=384592083&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/38459/20/coversum/k062135812_1.jpg", + "author": "마르쿠스 아우렐리우스 (지은이), 정미화 (옮긴이), 그레고리 헤이스 (해제)", + "isbn": "9791168273993" + } + }, + { + "original_file": "The Poetics of Aristotle by Aristotle.txt", + "translated_title": "시학", + "translated_author": "아리스토텔레스", + "aladin_data": { + "title": "아리스토텔레스 시학 (그리스어 원전 완역본)", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=265596201&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/26559/62/coversum/k392738937_1.jpg", + "author": "아리스토텔레스 (지은이), 박문재 (옮긴이)", + "isbn": "9791166812453" + } + }, + { + "original_file": "The Prince by Niccolò Machiavelli.txt", + "translated_title": "군주론", + "translated_author": "니콜로 마키아벨리", + "aladin_data": { + "title": "초판본 군주론 - 오리지널 초판본 표지디자인", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=249432298&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/24943/22/coversum/k032632692_2.jpg", + "author": "니콜로 마키아벨리 (지은이), 이시연 (옮긴이)", + "isbn": "9791164453085" + } + }, + { + "original_file": "The Principles of Psychology Volume 1 of 2 by William James.txt", + "translated_title": "심리학의 원리 제1권", + "translated_author": "윌리엄 제임스", + "aladin_data": {} + }, + { + "original_file": "The Problems of Philosophy by Bertrand Russell.txt", + "translated_title": "철학의 문제들", + "translated_author": "버트런드 러셀", + "aladin_data": { + "title": "철학의 문제들 - 전면 개역판", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=385198660&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/38519/86/coversum/8961474928_1.jpg", + "author": "버트런드 러셀 (지은이), 박영태 (옮긴이)", + "isbn": "9788961474924" + } + }, + { + "original_file": "The Prophet by Kahlil Gibran.txt", + "translated_title": "예언자", + "translated_author": "칼릴 지브란", + "aladin_data": { + "title": "예언자", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=129499645&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/12949/96/coversum/k672532485_1.jpg", + "author": "칼릴 지브란 (지은이), 류시화 (옮긴이)", + "isbn": "9791186686294" + } + }, + { + "original_file": "The Republic by Plato.txt", + "translated_title": "국가", + "translated_author": "플라톤", + "aladin_data": { + "title": "플라톤의 국가·정체(政體) - 개정 증보판", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=16812&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/1/68/coversum/8930606237_2.jpg", + "author": "플라톤 (지은이), 박종현 (옮긴이)", + "isbn": "9788930606233" + } + }, + { + "original_file": "The Republic of Plato by Plato.txt", + "translated_title": "국가", + "translated_author": "플라톤", + "aladin_data": { + "title": "플라톤의 국가·정체(政體) - 개정 증보판", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=16812&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/1/68/coversum/8930606237_2.jpg", + "author": "플라톤 (지은이), 박종현 (옮긴이)", + "isbn": "9788930606233" + } + }, + { + "original_file": "The Secret Doctrine Vol 1 of 4 by H P Blavatsky.txt", + "translated_title": "비밀 교리 1권", + "translated_author": "헬레나 페트로브나 블라바츠키", + "aladin_data": {} + }, + { + "original_file": "The Secret Doctrine Vol 2 of 4 by H P Blavatsky.txt", + "translated_title": "비밀 교리 제2권", + "translated_author": "헬레나 페트로브나 블라바츠키", + "aladin_data": {} + }, + { + "original_file": "The social contract discourses by Jean-Jacques Rousseau.txt", + "translated_title": "사회 계약론", + "translated_author": "장 자크 루소", + "aladin_data": { + "title": "사회계약론 - 자유와 평등을 위한 약속", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=139172090&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/13917/20/coversum/8961672398_1.jpg", + "author": "장 자크 루소 (지은이), 권혁 (옮긴이)", + "isbn": "9788961672399" + } + }, + { + "original_file": "The Song Celestial Or Bhagavad-Gîtâ from the Mahâbhârata.txt", + "translated_title": "신성한 노래", + "translated_author": "바가바드 기타", + "aladin_data": {} + }, + { + "original_file": "The symbolism of Freemasonry Illustrating and explaining its science and philosophy its legends myths and symbols by Albert Gallatin Mackey.txt", + "translated_title": "프리메이슨 상징학", + "translated_author": "앨버트 갤러틴 맥키", + "aladin_data": {} + }, + { + "original_file": "The Twilight of the Idols or How to Philosophize with the Hammer The Antichrist by Friedrich Wilhelm Nietzsche.txt", + "translated_title": "우상의 황혼, 혹은 망치로 철학하기 : 안티크리스트", + "translated_author": "프리드리히 빌헬름 니체", + "aladin_data": {} + }, + { + "original_file": "The Will to Believe and Other Essays in Popular Philosophy by William James.txt", + "translated_title": "의지의 힘", + "translated_author": "윌리엄 제임스", + "aladin_data": {} + }, + { + "original_file": "The World as Will and Idea Vol 1 of 3 by Arthur Schopenhauer.txt", + "translated_title": "의지와 표상으로서의 세계 1", + "translated_author": "아르투어 쇼펜하우어", + "aladin_data": { + "title": "의지와 표상으로서의 세계", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=95607072&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/9560/70/coversum/8949714221_2.jpg", + "author": "아르투어 쇼펜하우어 (지은이), 권기철 (옮긴이)", + "isbn": "9788949714226" + } + }, + { + "original_file": "Thus Spake Zarathustra A Book for All and None by Friedrich Wilhelm Nietzsche.txt", + "translated_title": "차라투스트라는 이렇게 말했다", + "translated_author": "프리드리히 니체", + "aladin_data": { + "title": "차라투스트라는 이렇게 말했다", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=454014&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/45/40/coversum/s352934786_1.jpg", + "author": "프리드리히 니체 (지은이), 장희창 (옮긴이)", + "isbn": "9788937460944" + } + }, + { + "original_file": "Utilitarianism by John Stuart Mill.txt", + "translated_title": "공리주의", + "translated_author": "존 스튜어트 밀", + "aladin_data": { + "title": "공리주의", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=243048009&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/24304/80/coversum/k452630592_1.jpg", + "author": "존 스튜어트 밀 (지은이), 이종인 (옮긴이)", + "isbn": "9791190878142" + } + }, + { + "original_file": "Utopia by Saint Thomas More.txt", + "translated_title": "유토피아", + "translated_author": "토머스 모어", + "aladin_data": { + "title": "유토피아", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=565805&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/56/58/coversum/8974832534_1.jpg", + "author": "토머스 모어 (지은이), 나종일 (옮긴이)", + "isbn": "9788974832537" + } + }, + { + "original_file": "Walden and On The Duty Of Civil Disobedience by Henry David Thoreau.txt", + "translated_title": "월든", + "translated_author": "헨리 데이비드 소로", + "aladin_data": { + "title": "월든 - 완결판", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=12840843&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/1284/8/coversum/8956605416_3.jpg", + "author": "헨리 데이비드 소로 (지은이), 강승영 (옮긴이)", + "isbn": "9788956605418" + } + }, + { + "original_file": "What Is Art by graf Leo Tolstoy.txt", + "translated_title": "예술이란 무엇인가", + "translated_author": "레프 톨스토이", + "aladin_data": { + "title": "예술이란 무엇인가", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=319767632&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/31976/76/coversum/k312834367_1.jpg", + "author": "레프 니콜라예비치 톨스토이 (지은이), 이강은 (옮긴이)", + "isbn": "9791166891694" + } + }, + { + "original_file": "新序 Chinese by Xiang Liu.txt", + "translated_title": "신서", + "translated_author": "유향", + "aladin_data": { + "title": "신서 1", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=6171917&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/617/19/coversum/8949705818_1.jpg", + "author": "유향 (지은이), 임동석 (옮긴이)", + "isbn": "9788949705811" + } + }, + { + "original_file": "日知錄 Chinese by Yanwu Gu.txt", + "translated_title": "일지록", + "translated_author": "고염무", + "aladin_data": { + "title": "원서발췌 일지록", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=383905503&partner=openAPI&start=api", + "thumbnail": "https://image.aladin.co.kr/product/38390/55/coversum/k522135566_1.jpg", + "author": "고염무 (지은이), 윤대식 (옮긴이)", + "isbn": "9791143017086" + } + }, + { + "original_file": "韓詩外傳 Complete Chinese by active 150 BC Ying Han.txt", + "translated_title": "한시외전", + "translated_author": "영한", + "aladin_data": {} + } +] \ No newline at end of file diff --git a/backend/scripts/generate_book_mapping.py b/backend/scripts/generate_book_mapping.py new file mode 100644 index 0000000..2166bf4 --- /dev/null +++ b/backend/scripts/generate_book_mapping.py @@ -0,0 +1,204 @@ +import os +import re +import json +import asyncio +import sys +from pathlib import Path +from dotenv import load_dotenv +import urllib.request +import urllib.parse +import google.generativeai as genai +from google.api_core.exceptions import ResourceExhausted + +backend_dir = Path(__file__).resolve().parents[1] +if str(backend_dir) not in sys.path: + sys.path.insert(0, str(backend_dir)) + +env_path = backend_dir.parent / ".env" +print(f"Loading .env from {env_path}, exists: {env_path.exists()}") +load_dotenv(dotenv_path=env_path) + +# Extract ALL GEMINI_API_KEYs from .env file +api_keys = [] +if env_path.exists(): + with open(env_path, 'r', encoding='utf-8') as f: + for line in f: + if "GEMINI_API_KEY=" in line: + key = line.split("GEMINI_API_KEY=")[1].strip() + if key not in api_keys: + api_keys.append(key) +else: + # Fallback to os.environ if .env missing + k = os.getenv("GEMINI_API_KEY") + if k: api_keys.append(k) + +# The user explicitly asked to start testing from new keys (lines 8~14), and then go back to line 2. +if len(api_keys) >= 4: + api_keys = api_keys[3:] + api_keys[:3] + +current_key_idx = 0 + +DATA_DIR = backend_dir / "data" +MAPPING_FILE = backend_dir / "data" / "books_mapping.json" +ALADIN_API_KEY = os.getenv("ALADIN_API_KEY") + +prompt_template = """You are given an English file name representing a philosophical book and its author. +Your task is to provide the standard, most well-known Korean translation title for this book, and the author's name in Korean. +Return ONLY JSON format exactly like this, without any markdown formatting or explanations: +{{"title": "한국어 번역본 책 제목", "author": "한국어 저자 이름"}} + +File name: {file_name} +""" + +async def kyobo_fallback(title: str, author: str) -> dict: + clean_title = title.replace(".txt", "").replace("_", " ") + query = f"{clean_title}".strip() + encoded_query = urllib.parse.quote(query) + url = f"https://search.kyobobook.co.kr/search?keyword={encoded_query}" + + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) + + loop = asyncio.get_running_loop() + def fetch(): + try: + with urllib.request.urlopen(req) as response: + return response.read().decode('utf-8') + except Exception as e: + print(f"Kyobo fetch error: {e}") + return None + + html = await loop.run_in_executor(None, fetch) + if not html: return {"kr_title": clean_title, "kr_author": author} + + match = re.search(r'(.*?)', html) + if match: + kr_title = match.group(1).strip() + kr_title = re.sub(r'<[^>]+>', '', kr_title) # Remove internal tags + return {"kr_title": kr_title, "kr_author": author} + + return {"kr_title": clean_title, "kr_author": author} + +async def translate_book_info(file_name: str) -> dict: + global current_key_idx + + while current_key_idx < len(api_keys): + key = api_keys[current_key_idx] + genai.configure(api_key=key) + # Using gemini-2.5-flash-lite as it has a higher free tier + model = genai.GenerativeModel("gemini-2.5-flash-lite") + + try: + response = await model.generate_content_async( + prompt_template.format(file_name=file_name), + generation_config=genai.types.GenerationConfig(temperature=0.7) + ) + result_text = response.text + clean_text = result_text.replace("```json", "").replace("```", "").strip() + data = json.loads(clean_text) + return {"kr_title": data["title"], "kr_author": data["author"]} + + except ResourceExhausted: + print(f"API Key {current_key_idx} exhausted. Switching to next key...") + current_key_idx += 1 + except Exception as e: + error_str = str(e).lower() + if "429" in error_str or "quota" in error_str or "exhausted" in error_str or "toomanyrequests" in error_str: + print(f"API Key {current_key_idx} exhausted. Switching to next key...") + current_key_idx += 1 + else: + print(f"Failed to parse LLM translation for {file_name}: {e}") + break + + # If all keys exhausted or other error, fallback + print(f"LLM Failed for {file_name}, falling back to Kyobo Search...") + return await kyobo_fallback(file_name, "") + +async def search_aladin(title: str, author: str) -> dict: + if not ALADIN_API_KEY: + print(f"Warning: ALADIN_API_KEY is not set. Skipping search for {title}") + return {} + + query = f"{title} {author}".strip() + url = f"http://www.aladin.co.kr/ttb/api/ItemSearch.aspx?ttbkey={ALADIN_API_KEY}&Query={urllib.parse.quote(query)}&QueryType=Keyword&MaxResults=1&start=1&SearchTarget=Book&output=js&Version=20131101" + + try: + loop = asyncio.get_running_loop() + def fetch(): + with urllib.request.urlopen(url) as response: + return response.read().decode('utf-8') + + response_text = await loop.run_in_executor(None, fetch) + data = json.loads(response_text) + + items = data.get("item", []) + if items: + item = items[0] + return { + "title": item.get("title", ""), + "link": item.get("link", ""), + "thumbnail": item.get("cover", ""), + "author": item.get("author", ""), + "isbn": item.get("isbn13", "") + } + except Exception as e: + print(f"Aladin API Error for {query}: {e}") + + return {} + +async def process_file(file_path: Path): + file_name = file_path.stem + print(f"Processing: {file_name}") + + translated = await translate_book_info(file_name) + kr_title = translated["kr_title"] + kr_author = translated["kr_author"] + + aladin_data = await search_aladin(kr_title, kr_author) + + return { + "original_file": file_path.name, + "translated_title": kr_title, + "translated_author": kr_author, + "aladin_data": aladin_data + } + +async def main(): + if not DATA_DIR.exists(): + print(f"Data directory not found at {DATA_DIR}") + return + + txt_files = list(DATA_DIR.glob("*.txt")) + print(f"Found {len(txt_files)} text files.") + + mapping = [] + if MAPPING_FILE.exists(): + with open(MAPPING_FILE, "r", encoding="utf-8") as f: + try: + mapping = json.load(f) + except json.JSONDecodeError: + mapping = [] + + existing_files = {item["original_file"] for item in mapping} + files_to_process = [f for f in txt_files if f.name not in existing_files] + + print(f"Skipping {len(existing_files)} already processed files. Processing {len(files_to_process)} new files.") + print(f"Loaded {len(api_keys)} API keys for rotation.") + + for i, f in enumerate(files_to_process): + try: + result = await process_file(f) + mapping.append(result) + + with open(MAPPING_FILE, "w", encoding="utf-8") as out_f: + json.dump(mapping, out_f, ensure_ascii=False, indent=4) + + print(f"Processed {i + 1}/{len(files_to_process)}, sleeping for 4s...") + await asyncio.sleep(4.1) + except Exception as e: + print(f"Error processing {f.name}: {e}") + await asyncio.sleep(4.1) + + print(f"Finished mapping. Total mapped: {len(mapping)}") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/scripts/generate_sql_updates.py b/backend/scripts/generate_sql_updates.py new file mode 100644 index 0000000..84cca81 --- /dev/null +++ b/backend/scripts/generate_sql_updates.py @@ -0,0 +1,61 @@ +import json +import os + +def generate_sql(): + # Load mapping + mapping_path = os.path.join("data", "books_mapping.json") + with open(mapping_path, "r", encoding="utf-8") as f: + mapping_data = json.load(f) + + # 1. Create a B-Tree index on the title field to make string matching instant + # instead of doing a full sequential table scan + sql_statements = [ + "CREATE INDEX IF NOT EXISTS idx_documents_book_title ON documents ((metadata->'book_info'->>'title'));\n", + "SET statement_timeout = '120s'; -- Increase timeout to be safe\n" + ] + + for book in mapping_data: + original_file = book.get("original_file", "") + name_without_ext = original_file[:-4] + parts = name_without_ext.rsplit(" by ", 1) + if len(parts) == 2: + title = parts[0].strip() + else: + title = name_without_ext + + title_to_match = f"Korean Translation of {title}" + + kr_title = book.get("translated_title", "") + aladin = book.get("aladin_data", {}) + thumbnail = aladin.get("thumbnail", "") + link = aladin.get("link", "") + + # Escape single quotes in strings for SQL + kr_title_esc = kr_title.replace("'", "''") + thumbnail_esc = thumbnail.replace("'", "''") + link_esc = link.replace("'", "''") + title_to_match_esc = title_to_match.replace("'", "''") + + jsonb_payload = json.dumps({ + "kr_title": kr_title, + "thumbnail": thumbnail, + "link": link + }, ensure_ascii=False) + + # Double the single quotes inside the json string for the SQL literal + jsonb_payload_esc = jsonb_payload.replace("'", "''") + + sql = f"""UPDATE documents +SET metadata = metadata || '{jsonb_payload_esc}'::jsonb +WHERE metadata->'book_info'->>'title' = '{title_to_match_esc}';""" + + sql_statements.append(sql) + + output_path = "update_metadata.sql" + with open(output_path, "w", encoding="utf-8") as f: + f.write("\n\n".join(sql_statements)) + + print(f"Generated {output_path} with {len(sql_statements)} UPDATE statements.") + +if __name__ == "__main__": + generate_sql() diff --git a/backend/scripts/update_db_metadata.py b/backend/scripts/update_db_metadata.py new file mode 100644 index 0000000..b0c6c68 --- /dev/null +++ b/backend/scripts/update_db_metadata.py @@ -0,0 +1,137 @@ +import os +import sys +import json +import asyncio +from pathlib import Path +from dotenv import load_dotenv + +backend_dir = Path(__file__).resolve().parents[1] +if str(backend_dir) not in sys.path: + sys.path.insert(0, str(backend_dir)) + +env_path = backend_dir.parent / ".env" +load_dotenv(dotenv_path=env_path) + +from app.services.database import get_client + +# Load mapping data +MAPPING_FILE = backend_dir / "data" / "books_mapping.json" + +async def update_database(): + print(f"Loading mapping from {MAPPING_FILE}") + if not MAPPING_FILE.exists(): + print("Mapping file not found.") + return + + with open(MAPPING_FILE, "r", encoding="utf-8") as f: + try: + mapping_data = json.load(f) + except Exception as e: + print(f"Error reading JSON: {e}") + return + + print(f"Found {len(mapping_data)} mapping entries.") + + supabase = get_client() + + success_count = 0 + error_count = 0 + + print("Fetching all document metadata ids to match locally...") + # Fetching all chunks to match metadata in memory to save DB query overhead/complexity. + # We only need id and metadata explicitly. + try: + # Since we might have many records, we paginate or fetch all + # Assuming < 10000 chunks for 100 books. Let's fetch all in one go or paginate. + all_docs = [] + limit = 1000 + offset = 0 + while True: + res = supabase.table("documents").select("id, metadata").range(offset, offset + limit - 1).execute() + all_docs.extend(res.data) + if len(res.data) < limit: + break + offset += limit + print(f"Total document chunks retrieved: {len(all_docs)}") + + except Exception as e: + print(f"Error fetching from supabase: {e}") + return + + print("\nStarting batch updates...") + + # Pre-process mapping data into a dictionary for quick lookup by 'title_to_match' + mapping_dict = {} + for book in mapping_data: + original_file = book.get("original_file", "") + # Parse filename exactly as ingest_all_data.py did to recreate the title. + name_without_ext = original_file[:-4] + parts = name_without_ext.rsplit(" by ", 1) + if len(parts) == 2: + title = parts[0].strip() + else: + title = name_without_ext + + # ingest_data.py stores this specific mock title in the DB: + title_to_match = f"Korean Translation of {title}" + mapping_dict[title_to_match] = book + + updates_to_make = [] + + if all_docs: + print("Sample metadata from DB:", all_docs[0]['metadata']) + + for doc in all_docs: + doc_id = doc['id'] + metadata = doc['metadata'] + + # The DB stores the title we want to match inside metadata->book_info->title + db_title = metadata.get('book_info', {}).get('title', '') + + # Determine which mapping record matches this chunk's DB title + matched_book = mapping_dict.get(db_title) + + if matched_book: + kr_title = matched_book.get("translated_title", "") + aladin = matched_book.get("aladin_data", {}) + thumbnail = aladin.get("thumbnail", "") + link = aladin.get("link", "") + + # Check if we actually need to update + if metadata.get("kr_title") != kr_title or metadata.get("thumbnail") != thumbnail or metadata.get("link") != link: + metadata["kr_title"] = kr_title + metadata["thumbnail"] = thumbnail + metadata["link"] = link + updates_to_make.append({"id": doc_id, "metadata": metadata}) + + print(f"Determined {len(updates_to_make)} chunks need metadata updates.") + + # Batch update using concurrent updates + if updates_to_make: + import concurrent.futures + + def update_doc(doc): + try: + supabase.table("documents").update({"metadata": doc["metadata"]}).eq("id", doc["id"]).execute() + return True + except Exception as e: + print(f"Error updating {doc['id']}: {e}") + return False + + print(f"Starting concurrent updates with 50 workers for {len(updates_to_make)} rows...") + processed = 0 + with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor: + for result in executor.map(update_doc, updates_to_make): + processed += 1 + if result: + success_count += 1 + else: + error_count += 1 + + if processed % 1000 == 0: + print(f"Processed {processed}/{len(updates_to_make)}...") + + print(f"\nUpdate complete! Success: {success_count}, Errors: {error_count}") + +if __name__ == "__main__": + asyncio.run(update_database()) diff --git a/backend/update_metadata.sql b/backend/update_metadata.sql new file mode 100644 index 0000000..f3a0fe6 --- /dev/null +++ b/backend/update_metadata.sql @@ -0,0 +1,409 @@ +CREATE INDEX IF NOT EXISTS idx_documents_book_title ON documents ((metadata->'book_info'->>'title')); + + +SET statement_timeout = '120s'; -- Increase timeout to be safe + + +UPDATE documents +SET metadata = metadata || '{"kr_title": "역설의 예산 1권", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of A Budget of Paradoxes Volume I'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "아는 자들을 위한 곤경", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of A Pickle for the Knowing Ones'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "인간 본성론", "thumbnail": "https://image.aladin.co.kr/product/435/90/coversum/8949705206_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=4359030&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of A Treatise of Human Nature'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "여권 옹호", "thumbnail": "https://image.aladin.co.kr/product/4569/0/coversum/8994054596_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=45690064&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of A Vindication of the Rights of Woman'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "차라투스트라는 이렇게 말했다", "thumbnail": "https://image.aladin.co.kr/product/45/40/coversum/s352934786_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=454014&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Also sprach Zarathustra Ein Buch für Alle und Keinen German'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "인간 오성론", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of An Enquiry Concerning Human Understanding'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "인간 오성론", "thumbnail": "https://image.aladin.co.kr/product/9059/21/coversum/k092535101_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=90592125&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of An Essay Concerning Humane Understanding Volume 1'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "소크라테스의 변명", "thumbnail": "https://image.aladin.co.kr/product/22/40/coversum/8931003714_3.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=224035&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Apology'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "소크라테스의 변론, 크리톤, 파이돈", "thumbnail": "https://image.aladin.co.kr/product/21679/27/coversum/k252636705_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=216792703&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Apology Crito and Phaedo of Socrates'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "생각하는 대로", "thumbnail": "https://image.aladin.co.kr/product/34558/80/coversum/k732933167_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=345588057&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of As a man thinketh'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "베이컨 수상록; 고대인의 지혜", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Bacons Essays and Wisdom of the Ancients'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "선악의 저편", "thumbnail": "https://image.aladin.co.kr/product/17492/31/coversum/8957336117_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=174923171&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Beyond Good and Evil'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "투스쿨룸 대화", "thumbnail": "https://image.aladin.co.kr/product/28633/67/coversum/8957337679_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=286336783&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Ciceros Tusculan Disputations'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "의무론", "thumbnail": "https://image.aladin.co.kr/product/85/24/coversum/8930606245_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=852420&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of De Officiis Latin'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "민주주의와 교육", "thumbnail": "https://image.aladin.co.kr/product/92/29/coversum/8925400669_2.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=922961&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Democracy and Education An Introduction to the Philosophy of Education'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "미국의 민주주의 2권", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Democracy in America Volume 2'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "악마학 및 악마 전승", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Demonology and Devil-lore'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "방법서설", "thumbnail": "https://image.aladin.co.kr/product/34798/32/coversum/k152933225_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=347983217&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Discourse on the Method of Rightly Conducting Ones Reason and of Seeking Truth in the Sciences'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "이 사람을 보라", "thumbnail": "https://image.aladin.co.kr/product/30235/68/coversum/8957338195_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=302356844&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Ecce Homo'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "에머슨 수상록", "thumbnail": "https://image.aladin.co.kr/product/10/4/coversum/8972432954_2.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=100408&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Essays by Ralph Waldo Emerson'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "인생론", "thumbnail": "https://image.aladin.co.kr/product/30866/59/coversum/8932440093_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=308665960&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Essays of Schopenhauer'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "에티카", "thumbnail": "https://image.aladin.co.kr/product/99/33/coversum/8930625460_2.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=993377&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Ethics'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "에밀리 포스트의 에티켓", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Etiquette'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "에우티프론", "thumbnail": "https://image.aladin.co.kr/product/27217/11/coversum/8957337342_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=272171162&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Euthyphro'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "윤리 형이상학 정초", "thumbnail": "https://image.aladin.co.kr/product/16835/56/coversum/8957336036_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=168355651&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Fundamental Principles of the Metaphysic of Morals'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "괴테의 색채론", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Goethes Theory of Colours'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "고르기아스", "thumbnail": "https://image.aladin.co.kr/product/26534/45/coversum/8957337210_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=265344583&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Gorgias'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "우리는 어떻게 생각하는가", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of How We Think'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "인간적인, 너무나 인간적인", "thumbnail": "https://image.aladin.co.kr/product/27/95/coversum/8970132619_3.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=279599&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Human All Too Human A Book for Free Spirits'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "베일 벗은 이시스", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Isis unveiled Volume 1 of 2 Science A master-key to mysteries of ancient and modern science and theology'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "법률", "thumbnail": "https://image.aladin.co.kr/product/464/64/coversum/8930606296_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=4646467&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Laws'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "리바이어던", "thumbnail": "https://image.aladin.co.kr/product/248/8/coversum/s392037901_2.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=2480851&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Leviathan'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "명상록", "thumbnail": "https://image.aladin.co.kr/product/38459/20/coversum/k062135812_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=384592083&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Meditations'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "자연", "thumbnail": "https://image.aladin.co.kr/product/3925/17/coversum/8956607648_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=39251790&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Nature'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "영웅숭배론", "thumbnail": "https://image.aladin.co.kr/product/31353/18/coversum/8935678147_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=313531822&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of On Heroes Hero-Worship and the Heroic in History'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "자유론", "thumbnail": "https://image.aladin.co.kr/product/38193/81/coversum/k302034718_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=381938135&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of On Liberty'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "시민 불복종", "thumbnail": "https://image.aladin.co.kr/product/28419/44/coversum/k742835213_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=284194464&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of On the Duty of Civil Disobedience'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "사물의 본성에 관하여", "thumbnail": "https://image.aladin.co.kr/product/1459/94/coversum/8957332227_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=14599483&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of On the Nature of Things'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "전쟁론", "thumbnail": "https://image.aladin.co.kr/product/8652/41/coversum/8961951424_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=86524117&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of On War'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "파스칼의 팡세", "thumbnail": "https://image.aladin.co.kr/product/36757/63/coversum/k952030294_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=367576319&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Pascals Pensées'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "영구 평화론", "thumbnail": "https://image.aladin.co.kr/product/288/17/coversum/8930610439_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=2881780&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Perpetual Peace A Philosophical Essay'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "파이돈", "thumbnail": "https://image.aladin.co.kr/product/21679/27/coversum/k252636705_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=216792703&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Phaedo'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "파이드로스", "thumbnail": "https://image.aladin.co.kr/product/182/6/coversum/8931005881_2.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=1820615&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Phaedrus'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "플라톤과 소크라테스의 동반자들", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Plato and the Other Companions of Sokrates 3rd ed Volume 1'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "플루타르코스 영웅전", "thumbnail": "https://image.aladin.co.kr/product/697/3/coversum/8991290337_2.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=6970308&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Plutarchs Morals'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "정치학", "thumbnail": "https://image.aladin.co.kr/product/439/98/coversum/8991290280_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=4399813&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Politics A Treatise on Government'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "실용주의: 어떤 오래된 사고방식에 대한 새로운 이름", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Pragmatism A New Name for Some Old Ways of Thinking'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "그리스 정신사: 영혼 숭배와 불멸에 대한 신념", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Psyche The Cult of Souls and Belief in Immortality among the Greeks'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "무의식의 심리학", "thumbnail": "https://image.aladin.co.kr/product/30020/50/coversum/k222838355_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=300205010&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Psychology of the Unconscious'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "잠언과 도덕적 격언", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Reflections or Sentences and Moral Maxims'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "신의 사랑에 대한 계시", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Revelations of Divine Love'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "로마 스토아주의", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Roman Stoicism'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "통치론", "thumbnail": "https://image.aladin.co.kr/product/30110/63/coversum/897291780x_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=301106377&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Second Treatise of Government'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "싯다르타", "thumbnail": "https://image.aladin.co.kr/product/32/95/coversum/s062934786_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=329596&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Siddhartha'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "손자병법", "thumbnail": "https://image.aladin.co.kr/product/37298/6/coversum/k292031545_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=372980631&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Sun Tzŭ on the Art of War The Oldest Military Treatise in the World'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "향연", "thumbnail": "https://image.aladin.co.kr/product/21679/27/coversum/k252636705_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=216792703&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Symposium'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "우울의 해부", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Anatomy of Melancholy'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "안티크리스트", "thumbnail": "https://image.aladin.co.kr/product/3542/50/coversum/8957333444_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=35425033&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Antichrist'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "비극의 탄생", "thumbnail": "https://image.aladin.co.kr/product/98/85/coversum/8957331077_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=988511&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Birth of Tragedy or Hellenism and Pessimism'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "우상의 황혼", "thumbnail": "https://image.aladin.co.kr/product/6419/39/coversum/8957334513_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=64193963&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Case of Wagner Nietzsche Contra Wagner and Selected Aphorisms'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "신의 도시 1", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The City of God Volume I'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "신의 도성 2", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The City of God Volume II'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "공산당 선언", "thumbnail": "https://image.aladin.co.kr/product/14325/74/coversum/k172532941_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=143257420&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Communist Manifesto'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "고백록", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Confessions of St Augustine'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "철학의 위안", "thumbnail": "https://image.aladin.co.kr/product/14712/19/coversum/k002532053_2.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=147121964&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Consolation of Philosophy'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "순수이성비판", "thumbnail": "https://image.aladin.co.kr/product/66/97/coversum/8957330836_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=669748&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Critique of Pure Reason'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "편람", "thumbnail": "https://image.aladin.co.kr/product/23132/6/coversum/k022637708_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=231320657&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Enchiridion'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "쇼펜하우어 철학 에세이: 비관주의 연구", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Essays of Arthur Schopenhauer Studies in Pessimism'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "행복론", "thumbnail": "https://image.aladin.co.kr/product/30866/59/coversum/8932440093_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=308665960&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Essays of Arthur Schopenhauer the Wisdom of Life'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "니코마코스 윤리학", "thumbnail": "https://image.aladin.co.kr/product/3168/56/coversum/8991290523_3.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=31685631&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Ethics of Aristotle'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "도덕의 계보", "thumbnail": "https://image.aladin.co.kr/product/27464/78/coversum/8957337350_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=274647853&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Genealogy of Morals'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "대심문관", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Grand Inquisitor'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "마법의 역사", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The history of magic including a clear and precise exposition of its procedure its rites and its mysteries'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "카마수트라", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Kama Sutra of Vatsyayana'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "위대한 철학자들의 생애와 사상", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Lives and Opinions of Eminent Philosophers'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "목요일의 사나이", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Man Who Was Thursday A Nightmare'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "천국과 지옥의 결혼", "thumbnail": "https://image.aladin.co.kr/product/14/11/coversum/8937418460_2.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=141182&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Marriage of Heaven and Hell'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "마르쿠스 아우렐리우스 황제의 명상록", "thumbnail": "https://image.aladin.co.kr/product/38459/20/coversum/k062135812_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=384592083&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Meditations of the Emperor Marcus Aurelius Antoninus'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "시학", "thumbnail": "https://image.aladin.co.kr/product/26559/62/coversum/k392738937_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=265596201&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Poetics of Aristotle'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "군주론", "thumbnail": "https://image.aladin.co.kr/product/24943/22/coversum/k032632692_2.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=249432298&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Prince'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "심리학의 원리 제1권", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Principles of Psychology Volume 1 of 2'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "철학의 문제들", "thumbnail": "https://image.aladin.co.kr/product/38519/86/coversum/8961474928_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=385198660&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Problems of Philosophy'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "예언자", "thumbnail": "https://image.aladin.co.kr/product/12949/96/coversum/k672532485_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=129499645&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Prophet'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "국가", "thumbnail": "https://image.aladin.co.kr/product/1/68/coversum/8930606237_2.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=16812&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Republic'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "국가", "thumbnail": "https://image.aladin.co.kr/product/1/68/coversum/8930606237_2.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=16812&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Republic of Plato'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "비밀 교리 1권", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Secret Doctrine Vol 1 of 4'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "비밀 교리 제2권", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Secret Doctrine Vol 2 of 4'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "사회 계약론", "thumbnail": "https://image.aladin.co.kr/product/13917/20/coversum/8961672398_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=139172090&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The social contract discourses'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "신성한 노래", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Song Celestial Or Bhagavad-Gîtâ from the Mahâbhârata'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "프리메이슨 상징학", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The symbolism of Freemasonry Illustrating and explaining its science and philosophy its legends myths and symbols'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "우상의 황혼, 혹은 망치로 철학하기 : 안티크리스트", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Twilight of the Idols or How to Philosophize with the Hammer The Antichrist'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "의지의 힘", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The Will to Believe and Other Essays in Popular Philosophy'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "의지와 표상으로서의 세계 1", "thumbnail": "https://image.aladin.co.kr/product/9560/70/coversum/8949714221_2.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=95607072&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of The World as Will and Idea Vol 1 of 3'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "차라투스트라는 이렇게 말했다", "thumbnail": "https://image.aladin.co.kr/product/45/40/coversum/s352934786_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=454014&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Thus Spake Zarathustra A Book for All and None'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "공리주의", "thumbnail": "https://image.aladin.co.kr/product/24304/80/coversum/k452630592_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=243048009&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Utilitarianism'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "유토피아", "thumbnail": "https://image.aladin.co.kr/product/56/58/coversum/8974832534_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=565805&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Utopia'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "월든", "thumbnail": "https://image.aladin.co.kr/product/1284/8/coversum/8956605416_3.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=12840843&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of Walden and On The Duty Of Civil Disobedience'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "예술이란 무엇인가", "thumbnail": "https://image.aladin.co.kr/product/31976/76/coversum/k312834367_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=319767632&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of What Is Art'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "신서", "thumbnail": "https://image.aladin.co.kr/product/617/19/coversum/8949705818_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=6171917&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of 新序 Chinese'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "일지록", "thumbnail": "https://image.aladin.co.kr/product/38390/55/coversum/k522135566_1.jpg", "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=383905503&partner=openAPI&start=api"}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of 日知錄 Chinese'; + +UPDATE documents +SET metadata = metadata || '{"kr_title": "한시외전", "thumbnail": "", "link": ""}'::jsonb +WHERE metadata->'book_info'->>'title' = 'Korean Translation of 韓詩外傳 Complete Chinese'; \ No newline at end of file diff --git a/frontend/components/chat/MessageList.tsx b/frontend/components/chat/MessageList.tsx index f60232a..09af532 100644 --- a/frontend/components/chat/MessageList.tsx +++ b/frontend/components/chat/MessageList.tsx @@ -72,7 +72,11 @@ export function MessageList({ messages, onOpenCitation }: Props) { {/* Citation Cards if metadata exists */} {msg.metadata && msg.metadata.length > 0 && Array.from(new Map(msg.metadata.map((m) => [m.id, m])).values()).map((meta) => { - const title = meta.book_info.title; + const title = meta.kr_title || meta.book_info?.title || meta.id; + // Use newly added 'thumbnail' fallback to cover_url + const coverUrl = meta.thumbnail || (meta.book_info?.cover_url !== "https://image.aladin.co.kr/product/dummy" ? meta.book_info?.cover_url : ""); + const bookLink = meta.link || meta.book_info?.link; + const isClickable = Boolean(onOpenCitation); const interactiveProps = isClickable ? { @@ -91,34 +95,55 @@ export function MessageList({ messages, onOpenCitation }: Props) {
-
- {meta.book_info.cover_url && !meta.book_info.cover_url.includes("dummy") ? ( - <> - {/* eslint-disable-next-line @next/next/no-img-element */} - {title} - - ) : ( - {meta.scholar.charAt(0)} +
+
+ {coverUrl && !coverUrl.includes("dummy") ? ( + <> + {/* eslint-disable-next-line @next/next/no-img-element */} + {title} + + ) : ( + {meta.scholar.charAt(0)} + )} +
+
+
+ Ref +

+ {meta.scholar} • {meta.school} +

+
+
{title}
+ + {bookLink && bookLink !== "https://www.aladin.co.kr/dummy-link" && ( + + )} +
+ {onOpenCitation && ( +
+ +
)}
-
-
참조: {title}
-

- {meta.scholar} - {meta.school} -

-
- {onOpenCitation && ( - - )}
) })} diff --git a/frontend/types/chat.ts b/frontend/types/chat.ts index 4573a31..88c21cb 100644 --- a/frontend/types/chat.ts +++ b/frontend/types/chat.ts @@ -10,6 +10,9 @@ export interface DocumentMetadata { scholar: string; book_info: BookInfo; chunk_index: number; + kr_title?: string; + thumbnail?: string; + link?: string; } export interface Message { From 8a01e1d05e1cd9a2b7ac281d1b0867a0eeaa8273 Mon Sep 17 00:00:00 2001 From: Sanghun95 Date: Mon, 2 Mar 2026 22:49:26 +0900 Subject: [PATCH 02/52] fix: apply coderabbit review suggestions --- backend/app/services/llm.py | 4 +- backend/scripts/check_models.py | 20 + backend/scripts/generate_book_mapping.py | 32 +- backend/scripts/generate_sql_updates.py | 5 +- backend/scripts/update_db_metadata.py | 30 +- backend/update_metadata.sql | 5 +- frontend/components/chat/MessageList.tsx | 7 +- pr_commet.txt | 599 +++++++++++++++++++++++ 8 files changed, 671 insertions(+), 31 deletions(-) create mode 100644 backend/scripts/check_models.py create mode 100644 pr_commet.txt diff --git a/backend/app/services/llm.py b/backend/app/services/llm.py index 07b7c9d..df08de6 100644 --- a/backend/app/services/llm.py +++ b/backend/app/services/llm.py @@ -23,7 +23,9 @@ def get_all_gemini_keys() -> list[str]: # Find all variations of GEMINI_API_KEY assignments matches = re.findall(r'(?:#\s*)?GEMINI_API_KEY\s*=\s*(.+)', content) for m in matches: - key = m.strip() + # Remove inline comments and strip quotes + m = re.split(r'\s+#', m, 1)[0] + key = m.strip().strip('"').strip("'") if key and key not in keys: keys.append(key) diff --git a/backend/scripts/check_models.py b/backend/scripts/check_models.py new file mode 100644 index 0000000..13a2586 --- /dev/null +++ b/backend/scripts/check_models.py @@ -0,0 +1,20 @@ +import os +from pathlib import Path +from dotenv import load_dotenv +import google.generativeai as genai + +env_path = Path(__file__).resolve().parents[2] / ".env" +load_dotenv(dotenv_path=env_path) + +api_key = os.getenv("GEMINI_API_KEY") +if not api_key: + print("No API key found!") +else: + genai.configure(api_key=api_key) + print("Available Models:") + try: + for m in genai.list_models(): + if 'generateContent' in m.supported_generation_methods: + print(m.name) + except Exception as e: + print(f"Error listing models: {e}") diff --git a/backend/scripts/generate_book_mapping.py b/backend/scripts/generate_book_mapping.py index 2166bf4..a80a9ad 100644 --- a/backend/scripts/generate_book_mapping.py +++ b/backend/scripts/generate_book_mapping.py @@ -7,6 +7,7 @@ from dotenv import load_dotenv import urllib.request import urllib.parse +import urllib.error import google.generativeai as genai from google.api_core.exceptions import ResourceExhausted @@ -23,14 +24,17 @@ if env_path.exists(): with open(env_path, 'r', encoding='utf-8') as f: for line in f: - if "GEMINI_API_KEY=" in line: - key = line.split("GEMINI_API_KEY=")[1].strip() - if key not in api_keys: - api_keys.append(key) + m = re.match(r'^\s*GEMINI_API_KEY\s*=\s*(.+?)\s*(?:#.*)?$', line) + if not m: + continue + key = m.group(1).strip().strip('"').strip("'") + if key and key not in api_keys: + api_keys.append(key) else: # Fallback to os.environ if .env missing k = os.getenv("GEMINI_API_KEY") - if k: api_keys.append(k) + if k: + api_keys.append(k) # The user explicitly asked to start testing from new keys (lines 8~14), and then go back to line 2. if len(api_keys) >= 4: @@ -61,14 +65,18 @@ async def kyobo_fallback(title: str, author: str) -> dict: loop = asyncio.get_running_loop() def fetch(): try: - with urllib.request.urlopen(req) as response: + with urllib.request.urlopen(req, timeout=10) as response: return response.read().decode('utf-8') - except Exception as e: - print(f"Kyobo fetch error: {e}") + except urllib.error.URLError as e: + print(f"Kyobo network error: {e}") return None + except Exception as e: + print(f"Kyobo undefined error: {e}") + raise html = await loop.run_in_executor(None, fetch) - if not html: return {"kr_title": clean_title, "kr_author": author} + if not html: + return {"kr_title": clean_title, "kr_author": author} match = re.search(r'(.*?)', html) if match: @@ -124,7 +132,7 @@ async def search_aladin(title: str, author: str) -> dict: try: loop = asyncio.get_running_loop() def fetch(): - with urllib.request.urlopen(url) as response: + with urllib.request.urlopen(url, timeout=10) as response: return response.read().decode('utf-8') response_text = await loop.run_in_executor(None, fetch) @@ -189,8 +197,10 @@ async def main(): result = await process_file(f) mapping.append(result) - with open(MAPPING_FILE, "w", encoding="utf-8") as out_f: + tmp_file = MAPPING_FILE.with_suffix(".json.tmp") + with open(tmp_file, "w", encoding="utf-8") as out_f: json.dump(mapping, out_f, ensure_ascii=False, indent=4) + tmp_file.replace(MAPPING_FILE) print(f"Processed {i + 1}/{len(files_to_process)}, sleeping for 4s...") await asyncio.sleep(4.1) diff --git a/backend/scripts/generate_sql_updates.py b/backend/scripts/generate_sql_updates.py index 84cca81..f6755b2 100644 --- a/backend/scripts/generate_sql_updates.py +++ b/backend/scripts/generate_sql_updates.py @@ -16,7 +16,7 @@ def generate_sql(): for book in mapping_data: original_file = book.get("original_file", "") - name_without_ext = original_file[:-4] + name_without_ext = os.path.splitext(original_file)[0] parts = name_without_ext.rsplit(" by ", 1) if len(parts) == 2: title = parts[0].strip() @@ -31,9 +31,6 @@ def generate_sql(): link = aladin.get("link", "") # Escape single quotes in strings for SQL - kr_title_esc = kr_title.replace("'", "''") - thumbnail_esc = thumbnail.replace("'", "''") - link_esc = link.replace("'", "''") title_to_match_esc = title_to_match.replace("'", "''") jsonb_payload = json.dumps({ diff --git a/backend/scripts/update_db_metadata.py b/backend/scripts/update_db_metadata.py index b0c6c68..bd77027 100644 --- a/backend/scripts/update_db_metadata.py +++ b/backend/scripts/update_db_metadata.py @@ -17,7 +17,7 @@ # Load mapping data MAPPING_FILE = backend_dir / "data" / "books_mapping.json" -async def update_database(): +def update_database(): print(f"Loading mapping from {MAPPING_FILE}") if not MAPPING_FILE.exists(): print("Mapping file not found.") @@ -65,7 +65,7 @@ async def update_database(): for book in mapping_data: original_file = book.get("original_file", "") # Parse filename exactly as ingest_all_data.py did to recreate the title. - name_without_ext = original_file[:-4] + name_without_ext = Path(original_file).stem parts = name_without_ext.rsplit(" by ", 1) if len(parts) == 2: title = parts[0].strip() @@ -109,18 +109,24 @@ async def update_database(): # Batch update using concurrent updates if updates_to_make: import concurrent.futures + from time import sleep def update_doc(doc): - try: - supabase.table("documents").update({"metadata": doc["metadata"]}).eq("id", doc["id"]).execute() - return True - except Exception as e: - print(f"Error updating {doc['id']}: {e}") - return False - - print(f"Starting concurrent updates with 50 workers for {len(updates_to_make)} rows...") + max_retries = 3 + for attempt in range(max_retries): + try: + supabase.table("documents").update({"metadata": doc["metadata"]}).eq("id", doc["id"]).execute() + return True + except Exception as e: + if attempt < max_retries - 1: + sleep(0.5 * (attempt + 1)) # Exponential backoff + continue + print(f"Error updating {doc['id']}: {e}") + return False + + print(f"Starting concurrent updates with 10 workers for {len(updates_to_make)} rows...") processed = 0 - with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor: + with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: for result in executor.map(update_doc, updates_to_make): processed += 1 if result: @@ -134,4 +140,4 @@ def update_doc(doc): print(f"\nUpdate complete! Success: {success_count}, Errors: {error_count}") if __name__ == "__main__": - asyncio.run(update_database()) + update_database() diff --git a/backend/update_metadata.sql b/backend/update_metadata.sql index f3a0fe6..51b2a24 100644 --- a/backend/update_metadata.sql +++ b/backend/update_metadata.sql @@ -3,6 +3,7 @@ CREATE INDEX IF NOT EXISTS idx_documents_book_title ON documents ((metadata->'bo SET statement_timeout = '120s'; -- Increase timeout to be safe +BEGIN; UPDATE documents SET metadata = metadata || '{"kr_title": "역설의 예산 1권", "thumbnail": "", "link": ""}'::jsonb @@ -406,4 +407,6 @@ WHERE metadata->'book_info'->>'title' = 'Korean Translation of 日知錄 Chinese UPDATE documents SET metadata = metadata || '{"kr_title": "한시외전", "thumbnail": "", "link": ""}'::jsonb -WHERE metadata->'book_info'->>'title' = 'Korean Translation of 韓詩外傳 Complete Chinese'; \ No newline at end of file +WHERE metadata->'book_info'->>'title' = 'Korean Translation of 韓詩外傳 Complete Chinese'; + +COMMIT; \ No newline at end of file diff --git a/frontend/components/chat/MessageList.tsx b/frontend/components/chat/MessageList.tsx index 09af532..d71bebb 100644 --- a/frontend/components/chat/MessageList.tsx +++ b/frontend/components/chat/MessageList.tsx @@ -1,6 +1,9 @@ import { Sparkles, SquareArrowOutUpRight, ThumbsUp, Copy, RotateCcw } from "lucide-react"; import { Message, DocumentMetadata } from "../../types/chat"; +const DUMMY_COVER_URL = "https://image.aladin.co.kr/product/dummy"; +const DUMMY_BOOK_LINK = "https://www.aladin.co.kr/dummy-link"; + interface Props { messages: Message[]; onOpenCitation?: (meta: DocumentMetadata) => void; @@ -74,7 +77,7 @@ export function MessageList({ messages, onOpenCitation }: Props) { {msg.metadata && msg.metadata.length > 0 && Array.from(new Map(msg.metadata.map((m) => [m.id, m])).values()).map((meta) => { const title = meta.kr_title || meta.book_info?.title || meta.id; // Use newly added 'thumbnail' fallback to cover_url - const coverUrl = meta.thumbnail || (meta.book_info?.cover_url !== "https://image.aladin.co.kr/product/dummy" ? meta.book_info?.cover_url : ""); + const coverUrl = meta.thumbnail || (meta.book_info?.cover_url !== DUMMY_COVER_URL ? meta.book_info?.cover_url : ""); const bookLink = meta.link || meta.book_info?.link; const isClickable = Boolean(onOpenCitation); @@ -117,7 +120,7 @@ export function MessageList({ messages, onOpenCitation }: Props) {
{title}
- {bookLink && bookLink !== "https://www.aladin.co.kr/dummy-link" && ( + {bookLink && bookLink !== DUMMY_BOOK_LINK && (
'book_info'->>'title')); + + SET statement_timeout = '120s'; -- Increase timeout to be safe ++ ++BEGIN; + + UPDATE documents + SET metadata = metadata || '{"kr_title": "역설의 예산 1권", "thumbnail": "", "link": ""}'::jsonb + WHERE metadata->'book_info'->>'title' = 'Korean Translation of A Budget of Paradoxes Volume I'; +파일 끝에 COMMIT; 추가 + +🤖 Prompt for AI Agents +Verify each finding against the current code and only fix it if needed. + +In `@backend/update_metadata.sql` around lines 1 - 9, Wrap the UPDATE statements +in an explicit transaction to ensure atomicity: start a transaction (e.g. BEGIN) +before the CREATE INDEX / UPDATE documents statement block and finish with a +COMMIT (or ROLLBACK on error) so either all metadata updates (the UPDATE +documents that sets kr_title/thumbnail/link) succeed together or none do; locate +the block around CREATE INDEX IF NOT EXISTS idx_documents_book_title and the +UPDATE documents ... WHERE metadata->'book_info'->>'title' = 'Korean Translation +of A Budget of Paradoxes Volume I' and add the transaction boundaries +accordingly. +backend/app/services/llm.py (1) +24-28: API 키 파싱 시 엣지 케이스 처리 필요 + +현재 정규식은 값에 포함된 따옴표나 후행 주석을 처리하지 않습니다. 예를 들어: + +GEMINI_API_KEY="abc" → "abc" (따옴표 포함) +GEMINI_API_KEY=abc # comment → abc # comment (주석 포함) +♻️ 키 값 정제를 위한 개선안 + for m in matches: +- key = m.strip() ++ key = m.strip().strip('"').strip("'") ++ # Remove inline comments ++ if ' #' in key: ++ key = key.split(' #')[0].strip() + if key and key not in keys: + keys.append(key) +🤖 Prompt for AI Agents +Verify each finding against the current code and only fix it if needed. + +In `@backend/app/services/llm.py` around lines 24 - 28, The current parsing loop +that builds matches and assigns key is not stripping surrounding quotes or +removing inline comments; update the regex or post-process the capture in the +loop (the matches variable and the loop that sets key) so the extracted value +excludes trailing inline comments and surrounding single/double quotes. For +example, either change the pattern to capture quoted or unquoted values up to a +comment (e.g., use groups for "([^"]*)" or '([^']*)' or ([^#\n]+)) and pick the +non-empty group, or after getting m run m = re.split(r'\s+#', m, +1)[0].strip().strip('\'"') before appending to keys so values like +GEMINI_API_KEY="abc" and GEMINI_API_KEY=abc # comment yield clean abc. +frontend/components/chat/MessageList.tsx (1) +77-77: 더미 URL 상수 추출 권장 + +더미 URL 문자열이 여러 곳에 하드코딩되어 있습니다. 상수로 추출하면 유지보수성이 향상됩니다. + +♻️ 상수 추출 예시 +const DUMMY_COVER_URL = "https://image.aladin.co.kr/product/dummy"; +const DUMMY_BOOK_LINK = "https://www.aladin.co.kr/dummy-link"; + +// 사용 예시 +const coverUrl = meta.thumbnail || (meta.book_info?.cover_url !== DUMMY_COVER_URL ? meta.book_info?.cover_url : ""); +Also applies to: 102-102, 120-120 + +🤖 Prompt for AI Agents +Verify each finding against the current code and only fix it if needed. + +In `@frontend/components/chat/MessageList.tsx` at line 77, Multiple instances use +the hardcoded dummy URL string when computing coverUrl (e.g., the expression +meta.book_info?.cover_url !== "https://image.aladin.co.kr/product/dummy"), so +extract that literal into a shared constant (e.g., DUMMY_COVER_URL) and replace +all occurrences (including where coverUrl is computed and the other similar +spots) to improve maintainability; update relevant imports/exports if needed and +ensure comparisons use the new DUMMY_COVER_URL constant instead of the inline +string. +backend/scripts/generate_sql_updates.py (1) +18-24: 파일 확장자 처리 개선 권장 + +original_file[:-4]는 .txt 확장자(4글자)를 가정합니다. 다른 확장자의 파일이 있을 경우 의도치 않은 결과가 발생할 수 있습니다. + +♻️ Path를 사용한 안전한 확장자 처리 ++from pathlib import Path ++ + def generate_sql(): + # ... + for book in mapping_data: + original_file = book.get("original_file", "") +- name_without_ext = original_file[:-4] ++ name_without_ext = Path(original_file).stem + parts = name_without_ext.rsplit(" by ", 1) +🤖 Prompt for AI Agents +Verify each finding against the current code and only fix it if needed. + +In `@backend/scripts/generate_sql_updates.py` around lines 18 - 24, The code +currently trims the extension with original_file[:-4], which wrongly assumes a +3-char extension like ".txt"; replace this with +os.path.splitext(original_file)[0] to safely get the filename without extension +(ensure you import os), handle the case original_file == "" gracefully (e.g., +default to ""), and then continue to split name_without_ext with rsplit(" by ", +1) to derive title as done now; update occurrences of original_file, +name_without_ext, parts, and title accordingly. +backend/scripts/generate_book_mapping.py (1) +66-68: 광범위한 Exception 캐치는 장애 원인 분류와 복구 정책을 약화시킵니다. + +Line 66, Line 103, Line 143, Line 197에서 광범위 캐치를 사용하고 있어 네트워크 오류/파싱 오류/로직 오류가 동일하게 처리됩니다. 최소한 오류 타입을 분리해 재시도/중단 정책을 다르게 적용하는 편이 안전합니다. + +Also applies to: 103-110, 143-145, 197-199 + +🤖 Prompt for AI Agents +Verify each finding against the current code and only fix it if needed. + +In `@backend/scripts/generate_book_mapping.py` around lines 66 - 68, Replace broad +except Exception blocks that print "Kyobo fetch error: {e}" (and the similar +handlers at the other noted sites) with targeted exception handling: catch +network-related errors (e.g., requests.exceptions.RequestException) separately +from parsing/format errors (e.g., json.JSONDecodeError, ValueError) and +unexpected attribute/type errors (AttributeError, TypeError), log the exception +with context, and apply different recovery actions (retry/backoff for network +errors, abort/raise for parsing or logic errors). Update the handlers around the +Kyobo fetch code paths (the blocks that currently print the Kyobo fetch error) +to re-raise non-recoverable exceptions after logging and implement a simple +retry loop only for RequestException with a limited retry count. Ensure you +reference and modify the existing except blocks that contain the "Kyobo fetch +error" print so the change is localized. +🤖 Prompt for all review comments with AI agents +Verify each finding against the current code and only fix it if needed. + +Inline comments: +In `@backend/scripts/generate_book_mapping.py`: +- Line 33: The single-line if statements (e.g., "if k: api_keys.append(k)" and +the similar one later) trigger Ruff E701; replace each one-line form with a +standard multi-line if block: make the condition and the body on separate lines +and use indentation so the append call is on the next line under the if; update +both occurrences (the one using variable k and api_keys and the other similar +instance around line 71) to the expanded form to satisfy the linter. +- Around line 64-65: urlopen 호출에 timeout이 없어 외부 요청이 무기한 블록될 수 있으니, +generate_book_mapping.py 안의 두 곳(현재 req 변수로 호출하는 urllib.request.urlopen(req) +위치들)을 urllib.request.urlopen(req, timeout=...)로 변경해 적절한 타임아웃(예: 5-15초) 값을 전달하고, +호출부(예: 해당 함수들)에서 socket.timeout/urllib.error.URLError를 잡아 로깅하고 실패한 항목을 건너뛰거나 재시도 +로직으로 처리하도록 예외 처리를 추가하세요; 두 urlopen 호출을 모두 동일하게 수정해 일관된 동작을 보장하십시오. +- Around line 192-193: The code currently writes mapping directly to +MAPPING_FILE (json.dump(mapping, ...)) which can corrupt the file on +interruption; modify the write to use an atomic replace: write the JSON to a +temporary file in the same directory (e.g., via +tempfile.NamedTemporaryFile(delete=False) or similar), flush and fsync the temp +file, close it, then atomically replace MAPPING_FILE with os.replace(temp_path, +MAPPING_FILE); ensure you remove the temp file on error; apply this change +around the block that opens MAPPING_FILE and serializes "mapping". +- Around line 24-29: 현재 env file 파싱 블록(open(env_path, ...) for line in f)이 부분 +문자열로 주석 처리된 라인(# GEMINI_API_KEY=...)까지 잡고 따옴표나 인라인 주석을 제거하지 않으므로 잘못된 키가 들어갑니다; +수정 방법은 라인을 처리할 때 먼저 라인 앞뒤 공백을 strip하고 주석('#')로 시작하는 라인은 무시하며 "GEMINI_API_KEY="을 +찾을 때는 라인이 주석이 아닌 경우에만 처리하고, 값을 뽑은 뒤 inline comment( '#' 이후)를 제거하고 앞뒤 따옴표(' 또는 +")를 제거한 다음 빈 값이 아니면 api_keys 리스트에 추가하도록 바꿔라(참조: env_path file read loop, 변수 key, +리스트 api_keys). + +In `@backend/scripts/generate_sql_updates.py`: +- Around line 34-37: Remove the unnecessary escaped variables kr_title_esc, +thumbnail_esc, and link_esc from generate_sql_updates.py since the code uses +json.dumps and jsonb_payload_esc to handle escaping; keep title_to_match_esc if +it is actually used elsewhere. Search for the declarations of kr_title_esc, +thumbnail_esc, and link_esc and delete those lines plus any immediate unused +references, leaving the jsonb_payload_esc path intact. + +--- + +Nitpick comments: +In `@backend/app/services/llm.py`: +- Around line 24-28: The current parsing loop that builds matches and assigns +key is not stripping surrounding quotes or removing inline comments; update the +regex or post-process the capture in the loop (the matches variable and the loop +that sets key) so the extracted value excludes trailing inline comments and +surrounding single/double quotes. For example, either change the pattern to +capture quoted or unquoted values up to a comment (e.g., use groups for +"([^"]*)" or '([^']*)' or ([^#\n]+)) and pick the non-empty group, or after +getting m run m = re.split(r'\s+#', m, 1)[0].strip().strip('\'"') before +appending to keys so values like GEMINI_API_KEY="abc" and GEMINI_API_KEY=abc # +comment yield clean abc. + +In `@backend/scripts/generate_book_mapping.py`: +- Around line 66-68: Replace broad except Exception blocks that print "Kyobo +fetch error: {e}" (and the similar handlers at the other noted sites) with +targeted exception handling: catch network-related errors (e.g., +requests.exceptions.RequestException) separately from parsing/format errors +(e.g., json.JSONDecodeError, ValueError) and unexpected attribute/type errors +(AttributeError, TypeError), log the exception with context, and apply different +recovery actions (retry/backoff for network errors, abort/raise for parsing or +logic errors). Update the handlers around the Kyobo fetch code paths (the blocks +that currently print the Kyobo fetch error) to re-raise non-recoverable +exceptions after logging and implement a simple retry loop only for +RequestException with a limited retry count. Ensure you reference and modify the +existing except blocks that contain the "Kyobo fetch error" print so the change +is localized. + +In `@backend/scripts/generate_sql_updates.py`: +- Around line 18-24: The code currently trims the extension with +original_file[:-4], which wrongly assumes a 3-char extension like ".txt"; +replace this with os.path.splitext(original_file)[0] to safely get the filename +without extension (ensure you import os), handle the case original_file == "" +gracefully (e.g., default to ""), and then continue to split name_without_ext +with rsplit(" by ", 1) to derive title as done now; update occurrences of +original_file, name_without_ext, parts, and title accordingly. + +In `@backend/scripts/update_db_metadata.py`: +- Line 20: The function update_database() is declared async but never uses await +and the Supabase client is synchronous; remove the unnecessary async keyword and +make update_database a regular function, update any places that call it to stop +awaiting it (remove await or adjust to call synchronously), and run tests to +ensure no callers expect a coroutine; reference the update_database() definition +when making the change. +- Line 68: The slice original_file[:-4] assumes a 4-character extension; replace +it with Path(original_file).stem to safely get the filename without extension. +Update the code in update_db_metadata.py where name_without_ext is assigned +(currently name_without_ext = original_file[:-4]) to use Path(...).stem, and add +"from pathlib import Path" at the top if not already present so other code using +name_without_ext continues to work with the correct base name. +- Around line 121-133: The current concurrent update loop uses +ThreadPoolExecutor(max_workers=50) and lacks retry logic, which risks hitting +Supabase rate limits and losing updates; reduce concurrency (e.g., to 5-10) by +changing ThreadPoolExecutor(max_workers=50) and add retry-with-backoff inside +update_doc (introduce parameters like max_retries and base_delay), so update_doc +catches transient errors (including HTTP 429 rate-limit responses), sleeps with +exponential backoff plus jitter between attempts, and returns success/failure +only after retries are exhausted; ensure existing counters (success_count, +error_count) and the processed reporting remain unchanged and that update_doc +logs retry attempts for observability. + +In `@backend/update_metadata.sql`: +- Around line 1-9: Wrap the UPDATE statements in an explicit transaction to +ensure atomicity: start a transaction (e.g. BEGIN) before the CREATE INDEX / +UPDATE documents statement block and finish with a COMMIT (or ROLLBACK on error) +so either all metadata updates (the UPDATE documents that sets +kr_title/thumbnail/link) succeed together or none do; locate the block around +CREATE INDEX IF NOT EXISTS idx_documents_book_title and the UPDATE documents ... +WHERE metadata->'book_info'->>'title' = 'Korean Translation of A Budget of +Paradoxes Volume I' and add the transaction boundaries accordingly. + +In `@frontend/components/chat/MessageList.tsx`: +- Line 77: Multiple instances use the hardcoded dummy URL string when computing +coverUrl (e.g., the expression meta.book_info?.cover_url !== +"https://image.aladin.co.kr/product/dummy"), so extract that literal into a +shared constant (e.g., DUMMY_COVER_URL) and replace all occurrences (including +where coverUrl is computed and the other similar spots) to improve +maintainability; update relevant imports/exports if needed and ensure +comparisons use the new DUMMY_COVER_URL constant instead of the inline string. +ℹ️ Review info +Configuration used: Organization UI + +Review profile: CHILL + +Plan: Pro + +📥 Commits +Reviewing files that changed from the base of the PR and between e4f87e1 and 5a5407a. + +📒 Files selected for processing (9) +backend/app/core/config.py +backend/app/services/llm.py +backend/data/books_mapping.json +backend/scripts/generate_book_mapping.py +backend/scripts/generate_sql_updates.py +backend/scripts/update_db_metadata.py +backend/update_metadata.sql +frontend/components/chat/MessageList.tsx +frontend/types/chat.ts +In backend/scripts/generate_book_mapping.py: + +> + with open(env_path, 'r', encoding='utf-8') as f: ++ for line in f: ++ if "GEMINI_API_KEY=" in line: ++ key = line.split("GEMINI_API_KEY=")[1].strip() ++ if key not in api_keys: ++ api_keys.append(key) +⚠️ Potential issue | 🟠 Major + +.env 키 파싱이 주석/따옴표를 API 키로 포함할 수 있습니다. + +Line 26은 부분 문자열 매칭이라 # GEMINI_API_KEY=...도 잡힙니다. Line 27은 따옴표/인라인 주석 제거가 없어 잘못된 키가 로테이션에 들어갈 수 있습니다. + +🔧 제안 수정안 + if env_path.exists(): + with open(env_path, 'r', encoding='utf-8') as f: + for line in f: +- if "GEMINI_API_KEY=" in line: +- key = line.split("GEMINI_API_KEY=")[1].strip() +- if key not in api_keys: +- api_keys.append(key) ++ m = re.match(r'^\s*GEMINI_API_KEY\s*=\s*(.+?)\s*(?:#.*)?$', line) ++ if not m: ++ continue ++ key = m.group(1).strip().strip('"').strip("'") ++ if key and key not in api_keys: ++ api_keys.append(key) +🤖 Prompt for AI Agents +Verify each finding against the current code and only fix it if needed. + +In `@backend/scripts/generate_book_mapping.py` around lines 24 - 29, 현재 env file +파싱 블록(open(env_path, ...) for line in f)이 부분 문자열로 주석 처리된 라인(# +GEMINI_API_KEY=...)까지 잡고 따옴표나 인라인 주석을 제거하지 않으므로 잘못된 키가 들어갑니다; 수정 방법은 라인을 처리할 때 +먼저 라인 앞뒤 공백을 strip하고 주석('#')로 시작하는 라인은 무시하며 "GEMINI_API_KEY="을 찾을 때는 라인이 주석이 아닌 +경우에만 처리하고, 값을 뽑은 뒤 inline comment( '#' 이후)를 제거하고 앞뒤 따옴표(' 또는 ")를 제거한 다음 빈 값이 아니면 +api_keys 리스트에 추가하도록 바꿔라(참조: env_path file read loop, 변수 key, 리스트 api_keys). +In backend/scripts/generate_book_mapping.py: + +> +print(f"Loading .env from {env_path}, exists: {env_path.exists()}") ++load_dotenv(dotenv_path=env_path) ++ ++# Extract ALL GEMINI_API_KEYs from .env file ++api_keys = [] ++if env_path.exists(): ++ with open(env_path, 'r', encoding='utf-8') as f: ++ for line in f: ++ if "GEMINI_API_KEY=" in line: ++ key = line.split("GEMINI_API_KEY=")[1].strip() ++ if key not in api_keys: ++ api_keys.append(key) ++else: ++ # Fallback to os.environ if .env missing ++ k = os.getenv("GEMINI_API_KEY") ++ if k: api_keys.append(k) +⚠️ Potential issue | 🟡 Minor + +Ruff E701(한 줄 다중문)으로 린트 실패 가능성이 있습니다. + +Line 33, Line 71의 한 줄 if는 정적분석 오류(E701)로 보고됩니다. CI 린트 통과를 위해 분리해 주세요. + +🔧 제안 수정안 +- if k: api_keys.append(k) ++ if k: ++ api_keys.append(k) +... +- if not html: return {"kr_title": clean_title, "kr_author": author} ++ if not html: ++ return {"kr_title": clean_title, "kr_author": author} +Also applies to: 71-71 + +🧰 Tools 🪛 Ruff (0.15.2) +[error] 33-33: Multiple statements on one line (colon) + +(E701) + +🤖 Prompt for AI Agents +Verify each finding against the current code and only fix it if needed. + +In `@backend/scripts/generate_book_mapping.py` at line 33, The single-line if +statements (e.g., "if k: api_keys.append(k)" and the similar one later) trigger +Ruff E701; replace each one-line form with a standard multi-line if block: make +the condition and the body on separate lines and use indentation so the append +call is on the next line under the if; update both occurrences (the one using +variable k and api_keys and the other similar instance around line 71) to the +expanded form to satisfy the linter. +In backend/scripts/generate_book_mapping.py: + +> + with urllib.request.urlopen(req) as response: ++ return response.read().decode('utf-8') +⚠️ Potential issue | 🟠 Major + +🧩 Analysis chain +🏁 Script executed: + +cd backend/scripts && cat -n generate_book_mapping.py | head -80 +Repository: SanghunYun95/philo-rag + +Length of output: 3447 + +🏁 Script executed: + +cd backend/scripts && cat -n generate_book_mapping.py | sed -n '120,135p' +Repository: SanghunYun95/philo-rag + +Length of output: 825 + +🏁 Script executed: + +rg -n 'urlopen\(' backend/scripts/generate_book_mapping.py +Repository: SanghunYun95/philo-rag + +Length of output: 188 + +🏁 Script executed: + +rg -n 'urlopen\([^)]*timeout' backend/scripts/generate_book_mapping.py +Repository: SanghunYun95/philo-rag + +Length of output: 48 + +외부 네트워크 호출에 timeout이 없어 배치가 무기한 대기할 수 있습니다. + +64번 줄과 127번 줄의 urlopen 호출에 timeout이 설정되어 있지 않습니다. 외부 서비스가 응답 없을 경우 전체 매핑 프로세스가 멈출 수 있습니다. + +제안 수정안 +- with urllib.request.urlopen(req) as response: ++ with urllib.request.urlopen(req, timeout=10) as response: + return response.read().decode('utf-8') +- with urllib.request.urlopen(url) as response: ++ with urllib.request.urlopen(url, timeout=10) as response: + return response.read().decode('utf-8') +📝 Committable suggestion +‼️ IMPORTANT +Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. + +⬇️ Suggested change +- with urllib.request.urlopen(req) as response: +- return response.read().decode('utf-8') ++ with urllib.request.urlopen(req, timeout=10) as response: ++ return response.read().decode('utf-8') +🧰 Tools 🪛 Ruff (0.15.2) +[error] 64-64: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected. + +(S310) + +🤖 Prompt for AI Agents +Verify each finding against the current code and only fix it if needed. + +In `@backend/scripts/generate_book_mapping.py` around lines 64 - 65, urlopen 호출에 +timeout이 없어 외부 요청이 무기한 블록될 수 있으니, generate_book_mapping.py 안의 두 곳(현재 req 변수로 +호출하는 urllib.request.urlopen(req) 위치들)을 urllib.request.urlopen(req, timeout=...)로 +변경해 적절한 타임아웃(예: 5-15초) 값을 전달하고, 호출부(예: 해당 함수들)에서 +socket.timeout/urllib.error.URLError를 잡아 로깅하고 실패한 항목을 건너뛰거나 재시도 로직으로 처리하도록 예외 +처리를 추가하세요; 두 urlopen 호출을 모두 동일하게 수정해 일관된 동작을 보장하십시오. +In backend/scripts/generate_book_mapping.py: + +> + with open(MAPPING_FILE, "w", encoding="utf-8") as out_f: ++ json.dump(mapping, out_f, ensure_ascii=False, indent=4) +⚠️ Potential issue | 🟠 Major + +매핑 파일을 직접 overwrite하면 중단 시 JSON 손상 위험이 있습니다. + +Line 192-193은 바로 대상 파일에 쓰기 때문에 프로세스 중단/예외 시 books_mapping.json이 깨질 수 있습니다. 임시 파일 후 원자적 교체가 더 안전합니다. + +🔧 제안 수정안 +- with open(MAPPING_FILE, "w", encoding="utf-8") as out_f: +- json.dump(mapping, out_f, ensure_ascii=False, indent=4) ++ tmp_file = MAPPING_FILE.with_suffix(".json.tmp") ++ with open(tmp_file, "w", encoding="utf-8") as out_f: ++ json.dump(mapping, out_f, ensure_ascii=False, indent=4) ++ tmp_file.replace(MAPPING_FILE) +🤖 Prompt for AI Agents +Verify each finding against the current code and only fix it if needed. + +In `@backend/scripts/generate_book_mapping.py` around lines 192 - 193, The code +currently writes mapping directly to MAPPING_FILE (json.dump(mapping, ...)) +which can corrupt the file on interruption; modify the write to use an atomic +replace: write the JSON to a temporary file in the same directory (e.g., via +tempfile.NamedTemporaryFile(delete=False) or similar), flush and fsync the temp +file, close it, then atomically replace MAPPING_FILE with os.replace(temp_path, +MAPPING_FILE); ensure you remove the temp file on error; apply this change +around the block that opens MAPPING_FILE and serializes "mapping". +In backend/scripts/generate_sql_updates.py: + +> + kr_title_esc = kr_title.replace("'", "''") ++ thumbnail_esc = thumbnail.replace("'", "''") ++ link_esc = link.replace("'", "''") ++ title_to_match_esc = title_to_match.replace("'", "''") +⚠️ Potential issue | 🟡 Minor + +미사용 변수 제거 필요 + +kr_title_esc, thumbnail_esc, link_esc 변수가 선언되었지만 사용되지 않습니다. json.dumps + jsonb_payload_esc 방식으로 처리되므로 이 변수들은 불필요합니다. + +🧹 미사용 변수 제거 +- # Escape single quotes in strings for SQL +- kr_title_esc = kr_title.replace("'", "''") +- thumbnail_esc = thumbnail.replace("'", "''") +- link_esc = link.replace("'", "''") +- title_to_match_esc = title_to_match.replace("'", "''") +- + jsonb_payload = json.dumps({ + "kr_title": kr_title, + "thumbnail": thumbnail, + "link": link + }, ensure_ascii=False) + + # Double the single quotes inside the json string for the SQL literal + jsonb_payload_esc = jsonb_payload.replace("'", "''") ++ title_to_match_esc = title_to_match.replace("'", "''") +🧰 Tools 🪛 Ruff (0.15.2) +[error] 34-34: Local variable kr_title_esc is assigned to but never used + +Remove assignment to unused variable kr_title_esc + +(F841) + +[error] 35-35: Local variable thumbnail_esc is assigned to but never used + +Remove assignment to unused variable thumbnail_esc + +(F841) + +[error] 36-36: Local variable link_esc is assigned to but never used + +Remove assignment to unused variable link_esc + +(F841) + +🤖 Prompt for AI Agents +Verify each finding against the current code and only fix it if needed. + +In `@backend/scripts/generate_sql_updates.py` around lines 34 - 37, Remove the +unnecessary escaped variables kr_title_esc, thumbnail_esc, and link_esc from +generate_sql_updates.py since the code uses json.dumps and jsonb_payload_esc to +handle escaping; keep title_to_match_esc if it is actually used elsewhere. +Search for the declarations of kr_title_esc, thumbnail_esc, and link_esc and +delete those lines plus any immediate unused references, leaving the +jsonb_payload_esc path intact. \ No newline at end of file From 133442a6335d08e15c9dbd34fdaaa19d719f0131 Mon Sep 17 00:00:00 2001 From: Sanghun95 Date: Mon, 2 Mar 2026 23:18:45 +0900 Subject: [PATCH 03/52] fix(backend): apply coderabbit review feedback for db and mapping scripts --- .gitignore | Bin 274 -> 303 bytes backend/app/services/llm.py | 6 +- backend/scripts/check_models.py | 3 + backend/scripts/generate_book_mapping.py | 10 +- backend/scripts/generate_sql_updates.py | 11 +- pr_commet.txt | 599 ----------------------- 6 files changed, 22 insertions(+), 607 deletions(-) delete mode 100644 pr_commet.txt diff --git a/.gitignore b/.gitignore index d8d77c534002f3a79c97291e56a57c1856ee0901..efdf7723a5710da90955666f3a5797adac6cef18 100644 GIT binary patch delta 37 pcmbQlw4Q0gR%vC0fFOnB{M_8sypm$Bf}(gZyF{;~qGaOccmVj>4iW$W delta 9 QcmZ3_G>K`#)`^#50TZGGO8@`> diff --git a/backend/app/services/llm.py b/backend/app/services/llm.py index df08de6..216d84c 100644 --- a/backend/app/services/llm.py +++ b/backend/app/services/llm.py @@ -21,7 +21,11 @@ def get_all_gemini_keys() -> list[str]: with open(env_path, "r", encoding="utf-8") as f: content = f.read() # Find all variations of GEMINI_API_KEY assignments - matches = re.findall(r'(?:#\s*)?GEMINI_API_KEY\s*=\s*(.+)', content) + matches = re.findall( + r'^\s*GEMINI_API_KEY\s*=\s*(.+?)\s*(?:#.*)?$', + content, + flags=re.MULTILINE, + ) for m in matches: # Remove inline comments and strip quotes m = re.split(r'\s+#', m, 1)[0] diff --git a/backend/scripts/check_models.py b/backend/scripts/check_models.py index 13a2586..8feb061 100644 --- a/backend/scripts/check_models.py +++ b/backend/scripts/check_models.py @@ -6,9 +6,12 @@ env_path = Path(__file__).resolve().parents[2] / ".env" load_dotenv(dotenv_path=env_path) +import sys + api_key = os.getenv("GEMINI_API_KEY") if not api_key: print("No API key found!") + sys.exit(1) else: genai.configure(api_key=api_key) print("Available Models:") diff --git a/backend/scripts/generate_book_mapping.py b/backend/scripts/generate_book_mapping.py index a80a9ad..479e4f2 100644 --- a/backend/scripts/generate_book_mapping.py +++ b/backend/scripts/generate_book_mapping.py @@ -30,14 +30,16 @@ key = m.group(1).strip().strip('"').strip("'") if key and key not in api_keys: api_keys.append(key) -else: - # Fallback to os.environ if .env missing + +# Fallback to os.environ when parsing produced no key +if not api_keys: k = os.getenv("GEMINI_API_KEY") - if k: + if k and k not in api_keys: api_keys.append(k) # The user explicitly asked to start testing from new keys (lines 8~14), and then go back to line 2. -if len(api_keys) >= 4: +# Rotates keys only when running with ENABLE_TEST_KEY_ROTATION flag. +if os.getenv("ENABLE_TEST_KEY_ROTATION") and len(api_keys) >= 4: api_keys = api_keys[3:] + api_keys[:3] current_key_idx = 0 diff --git a/backend/scripts/generate_sql_updates.py b/backend/scripts/generate_sql_updates.py index f6755b2..369c518 100644 --- a/backend/scripts/generate_sql_updates.py +++ b/backend/scripts/generate_sql_updates.py @@ -1,15 +1,18 @@ import json import os +from pathlib import Path def generate_sql(): # Load mapping - mapping_path = os.path.join("data", "books_mapping.json") + backend_dir = Path(__file__).resolve().parents[1] + mapping_path = backend_dir / "data" / "books_mapping.json" with open(mapping_path, "r", encoding="utf-8") as f: mapping_data = json.load(f) # 1. Create a B-Tree index on the title field to make string matching instant # instead of doing a full sequential table scan sql_statements = [ + "BEGIN;\n", "CREATE INDEX IF NOT EXISTS idx_documents_book_title ON documents ((metadata->'book_info'->>'title'));\n", "SET statement_timeout = '120s'; -- Increase timeout to be safe\n" ] @@ -48,11 +51,13 @@ def generate_sql(): sql_statements.append(sql) - output_path = "update_metadata.sql" + sql_statements.append("COMMIT;") + + output_path = backend_dir / "update_metadata.sql" with open(output_path, "w", encoding="utf-8") as f: f.write("\n\n".join(sql_statements)) - print(f"Generated {output_path} with {len(sql_statements)} UPDATE statements.") + print(f"Generated {output_path} with {len(sql_statements)} statements (including BEGIN/COMMIT).") if __name__ == "__main__": generate_sql() diff --git a/pr_commet.txt b/pr_commet.txt deleted file mode 100644 index f470aee..0000000 --- a/pr_commet.txt +++ /dev/null @@ -1,599 +0,0 @@ -@coderabbitai[bot] commented on this pull request. - -Actionable comments posted: 5 - -🧹 Nitpick comments (8) -backend/scripts/update_db_metadata.py (3) -20-20: async 키워드가 불필요합니다 - -update_database() 함수가 async로 선언되어 있지만 내부에서 await를 사용하지 않습니다. Supabase 클라이언트도 동기식입니다. async를 제거하고 일반 함수로 변경하는 것이 코드 의도를 명확히 합니다. - -♻️ async 제거 --async def update_database(): -+def update_database(): - print(f"Loading mapping from {MAPPING_FILE}") - if __name__ == "__main__": -- asyncio.run(update_database()) -+ update_database() -🤖 Prompt for AI Agents -Verify each finding against the current code and only fix it if needed. - -In `@backend/scripts/update_db_metadata.py` at line 20, The function -update_database() is declared async but never uses await and the Supabase client -is synchronous; remove the unnecessary async keyword and make update_database a -regular function, update any places that call it to stop awaiting it (remove -await or adjust to call synchronously), and run tests to ensure no callers -expect a coroutine; reference the update_database() definition when making the -change. -68-68: 파일 확장자 처리 개선 권장 - -generate_sql_updates.py와 동일하게 original_file[:-4]는 .txt 확장자를 가정합니다. Path.stem을 사용하면 더 안전합니다. - -♻️ Path.stem 사용 -- name_without_ext = original_file[:-4] -+ name_without_ext = Path(original_file).stem -🤖 Prompt for AI Agents -Verify each finding against the current code and only fix it if needed. - -In `@backend/scripts/update_db_metadata.py` at line 68, The slice -original_file[:-4] assumes a 4-character extension; replace it with -Path(original_file).stem to safely get the filename without extension. Update -the code in update_db_metadata.py where name_without_ext is assigned (currently -name_without_ext = original_file[:-4]) to use Path(...).stem, and add "from -pathlib import Path" at the top if not already present so other code using -name_without_ext continues to work with the correct base name. -121-133: 동시 업데이트 워커 수 및 재시도 로직 검토 - -50개의 워커로 동시 업데이트 시 Supabase 요청 제한(rate limit)에 도달할 수 있습니다. 실패 시 재시도 로직이 없어 일시적 오류로 인한 업데이트 누락이 발생할 수 있습니다. - -♻️ 워커 수 감소 및 재시도 로직 추가 -+from time import sleep -+ - def update_doc(doc): -+ max_retries = 3 -+ for attempt in range(max_retries): -+ try: -+ supabase.table("documents").update({"metadata": doc["metadata"]}).eq("id", doc["id"]).execute() -+ return True -+ except Exception as e: -+ if attempt < max_retries - 1: -+ sleep(0.5 * (attempt + 1)) # Exponential backoff -+ continue -+ print(f"Error updating {doc['id']}: {e}") -+ return False -- try: -- supabase.table("documents").update({"metadata": doc["metadata"]}).eq("id", doc["id"]).execute() -- return True -- except Exception as e: -- print(f"Error updating {doc['id']}: {e}") -- return False - --print(f"Starting concurrent updates with 50 workers for {len(updates_to_make)} rows...") -+print(f"Starting concurrent updates with 10 workers for {len(updates_to_make)} rows...") - processed = 0 --with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor: -+with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: -🤖 Prompt for AI Agents -Verify each finding against the current code and only fix it if needed. - -In `@backend/scripts/update_db_metadata.py` around lines 121 - 133, The current -concurrent update loop uses ThreadPoolExecutor(max_workers=50) and lacks retry -logic, which risks hitting Supabase rate limits and losing updates; reduce -concurrency (e.g., to 5-10) by changing ThreadPoolExecutor(max_workers=50) and -add retry-with-backoff inside update_doc (introduce parameters like max_retries -and base_delay), so update_doc catches transient errors (including HTTP 429 -rate-limit responses), sleeps with exponential backoff plus jitter between -attempts, and returns success/failure only after retries are exhausted; ensure -existing counters (success_count, error_count) and the processed reporting -remain unchanged and that update_doc logs retry attempts for observability. -backend/update_metadata.sql (1) -1-9: 트랜잭션 래핑 고려 - -현재 UPDATE 문들이 트랜잭션으로 감싸져 있지 않아 일부 업데이트만 성공할 수 있습니다. 모든 업데이트의 원자성(atomicity)이 필요하다면 트랜잭션으로 감싸는 것을 권장합니다. - -♻️ 트랜잭션 래핑 예시 - CREATE INDEX IF NOT EXISTS idx_documents_book_title ON documents ((metadata->'book_info'->>'title')); - - SET statement_timeout = '120s'; -- Increase timeout to be safe -+ -+BEGIN; - - UPDATE documents - SET metadata = metadata || '{"kr_title": "역설의 예산 1권", "thumbnail": "", "link": ""}'::jsonb - WHERE metadata->'book_info'->>'title' = 'Korean Translation of A Budget of Paradoxes Volume I'; -파일 끝에 COMMIT; 추가 - -🤖 Prompt for AI Agents -Verify each finding against the current code and only fix it if needed. - -In `@backend/update_metadata.sql` around lines 1 - 9, Wrap the UPDATE statements -in an explicit transaction to ensure atomicity: start a transaction (e.g. BEGIN) -before the CREATE INDEX / UPDATE documents statement block and finish with a -COMMIT (or ROLLBACK on error) so either all metadata updates (the UPDATE -documents that sets kr_title/thumbnail/link) succeed together or none do; locate -the block around CREATE INDEX IF NOT EXISTS idx_documents_book_title and the -UPDATE documents ... WHERE metadata->'book_info'->>'title' = 'Korean Translation -of A Budget of Paradoxes Volume I' and add the transaction boundaries -accordingly. -backend/app/services/llm.py (1) -24-28: API 키 파싱 시 엣지 케이스 처리 필요 - -현재 정규식은 값에 포함된 따옴표나 후행 주석을 처리하지 않습니다. 예를 들어: - -GEMINI_API_KEY="abc" → "abc" (따옴표 포함) -GEMINI_API_KEY=abc # comment → abc # comment (주석 포함) -♻️ 키 값 정제를 위한 개선안 - for m in matches: -- key = m.strip() -+ key = m.strip().strip('"').strip("'") -+ # Remove inline comments -+ if ' #' in key: -+ key = key.split(' #')[0].strip() - if key and key not in keys: - keys.append(key) -🤖 Prompt for AI Agents -Verify each finding against the current code and only fix it if needed. - -In `@backend/app/services/llm.py` around lines 24 - 28, The current parsing loop -that builds matches and assigns key is not stripping surrounding quotes or -removing inline comments; update the regex or post-process the capture in the -loop (the matches variable and the loop that sets key) so the extracted value -excludes trailing inline comments and surrounding single/double quotes. For -example, either change the pattern to capture quoted or unquoted values up to a -comment (e.g., use groups for "([^"]*)" or '([^']*)' or ([^#\n]+)) and pick the -non-empty group, or after getting m run m = re.split(r'\s+#', m, -1)[0].strip().strip('\'"') before appending to keys so values like -GEMINI_API_KEY="abc" and GEMINI_API_KEY=abc # comment yield clean abc. -frontend/components/chat/MessageList.tsx (1) -77-77: 더미 URL 상수 추출 권장 - -더미 URL 문자열이 여러 곳에 하드코딩되어 있습니다. 상수로 추출하면 유지보수성이 향상됩니다. - -♻️ 상수 추출 예시 -const DUMMY_COVER_URL = "https://image.aladin.co.kr/product/dummy"; -const DUMMY_BOOK_LINK = "https://www.aladin.co.kr/dummy-link"; - -// 사용 예시 -const coverUrl = meta.thumbnail || (meta.book_info?.cover_url !== DUMMY_COVER_URL ? meta.book_info?.cover_url : ""); -Also applies to: 102-102, 120-120 - -🤖 Prompt for AI Agents -Verify each finding against the current code and only fix it if needed. - -In `@frontend/components/chat/MessageList.tsx` at line 77, Multiple instances use -the hardcoded dummy URL string when computing coverUrl (e.g., the expression -meta.book_info?.cover_url !== "https://image.aladin.co.kr/product/dummy"), so -extract that literal into a shared constant (e.g., DUMMY_COVER_URL) and replace -all occurrences (including where coverUrl is computed and the other similar -spots) to improve maintainability; update relevant imports/exports if needed and -ensure comparisons use the new DUMMY_COVER_URL constant instead of the inline -string. -backend/scripts/generate_sql_updates.py (1) -18-24: 파일 확장자 처리 개선 권장 - -original_file[:-4]는 .txt 확장자(4글자)를 가정합니다. 다른 확장자의 파일이 있을 경우 의도치 않은 결과가 발생할 수 있습니다. - -♻️ Path를 사용한 안전한 확장자 처리 -+from pathlib import Path -+ - def generate_sql(): - # ... - for book in mapping_data: - original_file = book.get("original_file", "") -- name_without_ext = original_file[:-4] -+ name_without_ext = Path(original_file).stem - parts = name_without_ext.rsplit(" by ", 1) -🤖 Prompt for AI Agents -Verify each finding against the current code and only fix it if needed. - -In `@backend/scripts/generate_sql_updates.py` around lines 18 - 24, The code -currently trims the extension with original_file[:-4], which wrongly assumes a -3-char extension like ".txt"; replace this with -os.path.splitext(original_file)[0] to safely get the filename without extension -(ensure you import os), handle the case original_file == "" gracefully (e.g., -default to ""), and then continue to split name_without_ext with rsplit(" by ", -1) to derive title as done now; update occurrences of original_file, -name_without_ext, parts, and title accordingly. -backend/scripts/generate_book_mapping.py (1) -66-68: 광범위한 Exception 캐치는 장애 원인 분류와 복구 정책을 약화시킵니다. - -Line 66, Line 103, Line 143, Line 197에서 광범위 캐치를 사용하고 있어 네트워크 오류/파싱 오류/로직 오류가 동일하게 처리됩니다. 최소한 오류 타입을 분리해 재시도/중단 정책을 다르게 적용하는 편이 안전합니다. - -Also applies to: 103-110, 143-145, 197-199 - -🤖 Prompt for AI Agents -Verify each finding against the current code and only fix it if needed. - -In `@backend/scripts/generate_book_mapping.py` around lines 66 - 68, Replace broad -except Exception blocks that print "Kyobo fetch error: {e}" (and the similar -handlers at the other noted sites) with targeted exception handling: catch -network-related errors (e.g., requests.exceptions.RequestException) separately -from parsing/format errors (e.g., json.JSONDecodeError, ValueError) and -unexpected attribute/type errors (AttributeError, TypeError), log the exception -with context, and apply different recovery actions (retry/backoff for network -errors, abort/raise for parsing or logic errors). Update the handlers around the -Kyobo fetch code paths (the blocks that currently print the Kyobo fetch error) -to re-raise non-recoverable exceptions after logging and implement a simple -retry loop only for RequestException with a limited retry count. Ensure you -reference and modify the existing except blocks that contain the "Kyobo fetch -error" print so the change is localized. -🤖 Prompt for all review comments with AI agents -Verify each finding against the current code and only fix it if needed. - -Inline comments: -In `@backend/scripts/generate_book_mapping.py`: -- Line 33: The single-line if statements (e.g., "if k: api_keys.append(k)" and -the similar one later) trigger Ruff E701; replace each one-line form with a -standard multi-line if block: make the condition and the body on separate lines -and use indentation so the append call is on the next line under the if; update -both occurrences (the one using variable k and api_keys and the other similar -instance around line 71) to the expanded form to satisfy the linter. -- Around line 64-65: urlopen 호출에 timeout이 없어 외부 요청이 무기한 블록될 수 있으니, -generate_book_mapping.py 안의 두 곳(현재 req 변수로 호출하는 urllib.request.urlopen(req) -위치들)을 urllib.request.urlopen(req, timeout=...)로 변경해 적절한 타임아웃(예: 5-15초) 값을 전달하고, -호출부(예: 해당 함수들)에서 socket.timeout/urllib.error.URLError를 잡아 로깅하고 실패한 항목을 건너뛰거나 재시도 -로직으로 처리하도록 예외 처리를 추가하세요; 두 urlopen 호출을 모두 동일하게 수정해 일관된 동작을 보장하십시오. -- Around line 192-193: The code currently writes mapping directly to -MAPPING_FILE (json.dump(mapping, ...)) which can corrupt the file on -interruption; modify the write to use an atomic replace: write the JSON to a -temporary file in the same directory (e.g., via -tempfile.NamedTemporaryFile(delete=False) or similar), flush and fsync the temp -file, close it, then atomically replace MAPPING_FILE with os.replace(temp_path, -MAPPING_FILE); ensure you remove the temp file on error; apply this change -around the block that opens MAPPING_FILE and serializes "mapping". -- Around line 24-29: 현재 env file 파싱 블록(open(env_path, ...) for line in f)이 부분 -문자열로 주석 처리된 라인(# GEMINI_API_KEY=...)까지 잡고 따옴표나 인라인 주석을 제거하지 않으므로 잘못된 키가 들어갑니다; -수정 방법은 라인을 처리할 때 먼저 라인 앞뒤 공백을 strip하고 주석('#')로 시작하는 라인은 무시하며 "GEMINI_API_KEY="을 -찾을 때는 라인이 주석이 아닌 경우에만 처리하고, 값을 뽑은 뒤 inline comment( '#' 이후)를 제거하고 앞뒤 따옴표(' 또는 -")를 제거한 다음 빈 값이 아니면 api_keys 리스트에 추가하도록 바꿔라(참조: env_path file read loop, 변수 key, -리스트 api_keys). - -In `@backend/scripts/generate_sql_updates.py`: -- Around line 34-37: Remove the unnecessary escaped variables kr_title_esc, -thumbnail_esc, and link_esc from generate_sql_updates.py since the code uses -json.dumps and jsonb_payload_esc to handle escaping; keep title_to_match_esc if -it is actually used elsewhere. Search for the declarations of kr_title_esc, -thumbnail_esc, and link_esc and delete those lines plus any immediate unused -references, leaving the jsonb_payload_esc path intact. - ---- - -Nitpick comments: -In `@backend/app/services/llm.py`: -- Around line 24-28: The current parsing loop that builds matches and assigns -key is not stripping surrounding quotes or removing inline comments; update the -regex or post-process the capture in the loop (the matches variable and the loop -that sets key) so the extracted value excludes trailing inline comments and -surrounding single/double quotes. For example, either change the pattern to -capture quoted or unquoted values up to a comment (e.g., use groups for -"([^"]*)" or '([^']*)' or ([^#\n]+)) and pick the non-empty group, or after -getting m run m = re.split(r'\s+#', m, 1)[0].strip().strip('\'"') before -appending to keys so values like GEMINI_API_KEY="abc" and GEMINI_API_KEY=abc # -comment yield clean abc. - -In `@backend/scripts/generate_book_mapping.py`: -- Around line 66-68: Replace broad except Exception blocks that print "Kyobo -fetch error: {e}" (and the similar handlers at the other noted sites) with -targeted exception handling: catch network-related errors (e.g., -requests.exceptions.RequestException) separately from parsing/format errors -(e.g., json.JSONDecodeError, ValueError) and unexpected attribute/type errors -(AttributeError, TypeError), log the exception with context, and apply different -recovery actions (retry/backoff for network errors, abort/raise for parsing or -logic errors). Update the handlers around the Kyobo fetch code paths (the blocks -that currently print the Kyobo fetch error) to re-raise non-recoverable -exceptions after logging and implement a simple retry loop only for -RequestException with a limited retry count. Ensure you reference and modify the -existing except blocks that contain the "Kyobo fetch error" print so the change -is localized. - -In `@backend/scripts/generate_sql_updates.py`: -- Around line 18-24: The code currently trims the extension with -original_file[:-4], which wrongly assumes a 3-char extension like ".txt"; -replace this with os.path.splitext(original_file)[0] to safely get the filename -without extension (ensure you import os), handle the case original_file == "" -gracefully (e.g., default to ""), and then continue to split name_without_ext -with rsplit(" by ", 1) to derive title as done now; update occurrences of -original_file, name_without_ext, parts, and title accordingly. - -In `@backend/scripts/update_db_metadata.py`: -- Line 20: The function update_database() is declared async but never uses await -and the Supabase client is synchronous; remove the unnecessary async keyword and -make update_database a regular function, update any places that call it to stop -awaiting it (remove await or adjust to call synchronously), and run tests to -ensure no callers expect a coroutine; reference the update_database() definition -when making the change. -- Line 68: The slice original_file[:-4] assumes a 4-character extension; replace -it with Path(original_file).stem to safely get the filename without extension. -Update the code in update_db_metadata.py where name_without_ext is assigned -(currently name_without_ext = original_file[:-4]) to use Path(...).stem, and add -"from pathlib import Path" at the top if not already present so other code using -name_without_ext continues to work with the correct base name. -- Around line 121-133: The current concurrent update loop uses -ThreadPoolExecutor(max_workers=50) and lacks retry logic, which risks hitting -Supabase rate limits and losing updates; reduce concurrency (e.g., to 5-10) by -changing ThreadPoolExecutor(max_workers=50) and add retry-with-backoff inside -update_doc (introduce parameters like max_retries and base_delay), so update_doc -catches transient errors (including HTTP 429 rate-limit responses), sleeps with -exponential backoff plus jitter between attempts, and returns success/failure -only after retries are exhausted; ensure existing counters (success_count, -error_count) and the processed reporting remain unchanged and that update_doc -logs retry attempts for observability. - -In `@backend/update_metadata.sql`: -- Around line 1-9: Wrap the UPDATE statements in an explicit transaction to -ensure atomicity: start a transaction (e.g. BEGIN) before the CREATE INDEX / -UPDATE documents statement block and finish with a COMMIT (or ROLLBACK on error) -so either all metadata updates (the UPDATE documents that sets -kr_title/thumbnail/link) succeed together or none do; locate the block around -CREATE INDEX IF NOT EXISTS idx_documents_book_title and the UPDATE documents ... -WHERE metadata->'book_info'->>'title' = 'Korean Translation of A Budget of -Paradoxes Volume I' and add the transaction boundaries accordingly. - -In `@frontend/components/chat/MessageList.tsx`: -- Line 77: Multiple instances use the hardcoded dummy URL string when computing -coverUrl (e.g., the expression meta.book_info?.cover_url !== -"https://image.aladin.co.kr/product/dummy"), so extract that literal into a -shared constant (e.g., DUMMY_COVER_URL) and replace all occurrences (including -where coverUrl is computed and the other similar spots) to improve -maintainability; update relevant imports/exports if needed and ensure -comparisons use the new DUMMY_COVER_URL constant instead of the inline string. -ℹ️ Review info -Configuration used: Organization UI - -Review profile: CHILL - -Plan: Pro - -📥 Commits -Reviewing files that changed from the base of the PR and between e4f87e1 and 5a5407a. - -📒 Files selected for processing (9) -backend/app/core/config.py -backend/app/services/llm.py -backend/data/books_mapping.json -backend/scripts/generate_book_mapping.py -backend/scripts/generate_sql_updates.py -backend/scripts/update_db_metadata.py -backend/update_metadata.sql -frontend/components/chat/MessageList.tsx -frontend/types/chat.ts -In backend/scripts/generate_book_mapping.py: - -> + with open(env_path, 'r', encoding='utf-8') as f: -+ for line in f: -+ if "GEMINI_API_KEY=" in line: -+ key = line.split("GEMINI_API_KEY=")[1].strip() -+ if key not in api_keys: -+ api_keys.append(key) -⚠️ Potential issue | 🟠 Major - -.env 키 파싱이 주석/따옴표를 API 키로 포함할 수 있습니다. - -Line 26은 부분 문자열 매칭이라 # GEMINI_API_KEY=...도 잡힙니다. Line 27은 따옴표/인라인 주석 제거가 없어 잘못된 키가 로테이션에 들어갈 수 있습니다. - -🔧 제안 수정안 - if env_path.exists(): - with open(env_path, 'r', encoding='utf-8') as f: - for line in f: -- if "GEMINI_API_KEY=" in line: -- key = line.split("GEMINI_API_KEY=")[1].strip() -- if key not in api_keys: -- api_keys.append(key) -+ m = re.match(r'^\s*GEMINI_API_KEY\s*=\s*(.+?)\s*(?:#.*)?$', line) -+ if not m: -+ continue -+ key = m.group(1).strip().strip('"').strip("'") -+ if key and key not in api_keys: -+ api_keys.append(key) -🤖 Prompt for AI Agents -Verify each finding against the current code and only fix it if needed. - -In `@backend/scripts/generate_book_mapping.py` around lines 24 - 29, 현재 env file -파싱 블록(open(env_path, ...) for line in f)이 부분 문자열로 주석 처리된 라인(# -GEMINI_API_KEY=...)까지 잡고 따옴표나 인라인 주석을 제거하지 않으므로 잘못된 키가 들어갑니다; 수정 방법은 라인을 처리할 때 -먼저 라인 앞뒤 공백을 strip하고 주석('#')로 시작하는 라인은 무시하며 "GEMINI_API_KEY="을 찾을 때는 라인이 주석이 아닌 -경우에만 처리하고, 값을 뽑은 뒤 inline comment( '#' 이후)를 제거하고 앞뒤 따옴표(' 또는 ")를 제거한 다음 빈 값이 아니면 -api_keys 리스트에 추가하도록 바꿔라(참조: env_path file read loop, 변수 key, 리스트 api_keys). -In backend/scripts/generate_book_mapping.py: - -> +print(f"Loading .env from {env_path}, exists: {env_path.exists()}") -+load_dotenv(dotenv_path=env_path) -+ -+# Extract ALL GEMINI_API_KEYs from .env file -+api_keys = [] -+if env_path.exists(): -+ with open(env_path, 'r', encoding='utf-8') as f: -+ for line in f: -+ if "GEMINI_API_KEY=" in line: -+ key = line.split("GEMINI_API_KEY=")[1].strip() -+ if key not in api_keys: -+ api_keys.append(key) -+else: -+ # Fallback to os.environ if .env missing -+ k = os.getenv("GEMINI_API_KEY") -+ if k: api_keys.append(k) -⚠️ Potential issue | 🟡 Minor - -Ruff E701(한 줄 다중문)으로 린트 실패 가능성이 있습니다. - -Line 33, Line 71의 한 줄 if는 정적분석 오류(E701)로 보고됩니다. CI 린트 통과를 위해 분리해 주세요. - -🔧 제안 수정안 -- if k: api_keys.append(k) -+ if k: -+ api_keys.append(k) -... -- if not html: return {"kr_title": clean_title, "kr_author": author} -+ if not html: -+ return {"kr_title": clean_title, "kr_author": author} -Also applies to: 71-71 - -🧰 Tools 🪛 Ruff (0.15.2) -[error] 33-33: Multiple statements on one line (colon) - -(E701) - -🤖 Prompt for AI Agents -Verify each finding against the current code and only fix it if needed. - -In `@backend/scripts/generate_book_mapping.py` at line 33, The single-line if -statements (e.g., "if k: api_keys.append(k)" and the similar one later) trigger -Ruff E701; replace each one-line form with a standard multi-line if block: make -the condition and the body on separate lines and use indentation so the append -call is on the next line under the if; update both occurrences (the one using -variable k and api_keys and the other similar instance around line 71) to the -expanded form to satisfy the linter. -In backend/scripts/generate_book_mapping.py: - -> + with urllib.request.urlopen(req) as response: -+ return response.read().decode('utf-8') -⚠️ Potential issue | 🟠 Major - -🧩 Analysis chain -🏁 Script executed: - -cd backend/scripts && cat -n generate_book_mapping.py | head -80 -Repository: SanghunYun95/philo-rag - -Length of output: 3447 - -🏁 Script executed: - -cd backend/scripts && cat -n generate_book_mapping.py | sed -n '120,135p' -Repository: SanghunYun95/philo-rag - -Length of output: 825 - -🏁 Script executed: - -rg -n 'urlopen\(' backend/scripts/generate_book_mapping.py -Repository: SanghunYun95/philo-rag - -Length of output: 188 - -🏁 Script executed: - -rg -n 'urlopen\([^)]*timeout' backend/scripts/generate_book_mapping.py -Repository: SanghunYun95/philo-rag - -Length of output: 48 - -외부 네트워크 호출에 timeout이 없어 배치가 무기한 대기할 수 있습니다. - -64번 줄과 127번 줄의 urlopen 호출에 timeout이 설정되어 있지 않습니다. 외부 서비스가 응답 없을 경우 전체 매핑 프로세스가 멈출 수 있습니다. - -제안 수정안 -- with urllib.request.urlopen(req) as response: -+ with urllib.request.urlopen(req, timeout=10) as response: - return response.read().decode('utf-8') -- with urllib.request.urlopen(url) as response: -+ with urllib.request.urlopen(url, timeout=10) as response: - return response.read().decode('utf-8') -📝 Committable suggestion -‼️ IMPORTANT -Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. - -⬇️ Suggested change -- with urllib.request.urlopen(req) as response: -- return response.read().decode('utf-8') -+ with urllib.request.urlopen(req, timeout=10) as response: -+ return response.read().decode('utf-8') -🧰 Tools 🪛 Ruff (0.15.2) -[error] 64-64: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected. - -(S310) - -🤖 Prompt for AI Agents -Verify each finding against the current code and only fix it if needed. - -In `@backend/scripts/generate_book_mapping.py` around lines 64 - 65, urlopen 호출에 -timeout이 없어 외부 요청이 무기한 블록될 수 있으니, generate_book_mapping.py 안의 두 곳(현재 req 변수로 -호출하는 urllib.request.urlopen(req) 위치들)을 urllib.request.urlopen(req, timeout=...)로 -변경해 적절한 타임아웃(예: 5-15초) 값을 전달하고, 호출부(예: 해당 함수들)에서 -socket.timeout/urllib.error.URLError를 잡아 로깅하고 실패한 항목을 건너뛰거나 재시도 로직으로 처리하도록 예외 -처리를 추가하세요; 두 urlopen 호출을 모두 동일하게 수정해 일관된 동작을 보장하십시오. -In backend/scripts/generate_book_mapping.py: - -> + with open(MAPPING_FILE, "w", encoding="utf-8") as out_f: -+ json.dump(mapping, out_f, ensure_ascii=False, indent=4) -⚠️ Potential issue | 🟠 Major - -매핑 파일을 직접 overwrite하면 중단 시 JSON 손상 위험이 있습니다. - -Line 192-193은 바로 대상 파일에 쓰기 때문에 프로세스 중단/예외 시 books_mapping.json이 깨질 수 있습니다. 임시 파일 후 원자적 교체가 더 안전합니다. - -🔧 제안 수정안 -- with open(MAPPING_FILE, "w", encoding="utf-8") as out_f: -- json.dump(mapping, out_f, ensure_ascii=False, indent=4) -+ tmp_file = MAPPING_FILE.with_suffix(".json.tmp") -+ with open(tmp_file, "w", encoding="utf-8") as out_f: -+ json.dump(mapping, out_f, ensure_ascii=False, indent=4) -+ tmp_file.replace(MAPPING_FILE) -🤖 Prompt for AI Agents -Verify each finding against the current code and only fix it if needed. - -In `@backend/scripts/generate_book_mapping.py` around lines 192 - 193, The code -currently writes mapping directly to MAPPING_FILE (json.dump(mapping, ...)) -which can corrupt the file on interruption; modify the write to use an atomic -replace: write the JSON to a temporary file in the same directory (e.g., via -tempfile.NamedTemporaryFile(delete=False) or similar), flush and fsync the temp -file, close it, then atomically replace MAPPING_FILE with os.replace(temp_path, -MAPPING_FILE); ensure you remove the temp file on error; apply this change -around the block that opens MAPPING_FILE and serializes "mapping". -In backend/scripts/generate_sql_updates.py: - -> + kr_title_esc = kr_title.replace("'", "''") -+ thumbnail_esc = thumbnail.replace("'", "''") -+ link_esc = link.replace("'", "''") -+ title_to_match_esc = title_to_match.replace("'", "''") -⚠️ Potential issue | 🟡 Minor - -미사용 변수 제거 필요 - -kr_title_esc, thumbnail_esc, link_esc 변수가 선언되었지만 사용되지 않습니다. json.dumps + jsonb_payload_esc 방식으로 처리되므로 이 변수들은 불필요합니다. - -🧹 미사용 변수 제거 -- # Escape single quotes in strings for SQL -- kr_title_esc = kr_title.replace("'", "''") -- thumbnail_esc = thumbnail.replace("'", "''") -- link_esc = link.replace("'", "''") -- title_to_match_esc = title_to_match.replace("'", "''") -- - jsonb_payload = json.dumps({ - "kr_title": kr_title, - "thumbnail": thumbnail, - "link": link - }, ensure_ascii=False) - - # Double the single quotes inside the json string for the SQL literal - jsonb_payload_esc = jsonb_payload.replace("'", "''") -+ title_to_match_esc = title_to_match.replace("'", "''") -🧰 Tools 🪛 Ruff (0.15.2) -[error] 34-34: Local variable kr_title_esc is assigned to but never used - -Remove assignment to unused variable kr_title_esc - -(F841) - -[error] 35-35: Local variable thumbnail_esc is assigned to but never used - -Remove assignment to unused variable thumbnail_esc - -(F841) - -[error] 36-36: Local variable link_esc is assigned to but never used - -Remove assignment to unused variable link_esc - -(F841) - -🤖 Prompt for AI Agents -Verify each finding against the current code and only fix it if needed. - -In `@backend/scripts/generate_sql_updates.py` around lines 34 - 37, Remove the -unnecessary escaped variables kr_title_esc, thumbnail_esc, and link_esc from -generate_sql_updates.py since the code uses json.dumps and jsonb_payload_esc to -handle escaping; keep title_to_match_esc if it is actually used elsewhere. -Search for the declarations of kr_title_esc, thumbnail_esc, and link_esc and -delete those lines plus any immediate unused references, leaving the -jsonb_payload_esc path intact. \ No newline at end of file From 43d1722305a32d533799e8041db85d4db84116c8 Mon Sep 17 00:00:00 2001 From: Sanghun95 Date: Mon, 2 Mar 2026 23:40:52 +0900 Subject: [PATCH 04/52] fix(backend): address additional coderabbit PR inline comments --- backend/app/services/llm.py | 2 +- backend/scripts/check_models.py | 16 +++++++++++----- backend/scripts/generate_book_mapping.py | 6 +++++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/backend/app/services/llm.py b/backend/app/services/llm.py index 216d84c..aa42417 100644 --- a/backend/app/services/llm.py +++ b/backend/app/services/llm.py @@ -13,7 +13,7 @@ _llm_lock = threading.Lock() def get_all_gemini_keys() -> list[str]: - """Reads all active and commented GEMINI_API_KEYs from the root .env file.""" + """Reads active GEMINI_API_KEY assignments from the root .env file.""" keys = [] env_path = Path(__file__).resolve().parents[3] / ".env" diff --git a/backend/scripts/check_models.py b/backend/scripts/check_models.py index 8feb061..1f8e348 100644 --- a/backend/scripts/check_models.py +++ b/backend/scripts/check_models.py @@ -8,11 +8,12 @@ import sys -api_key = os.getenv("GEMINI_API_KEY") -if not api_key: - print("No API key found!") - sys.exit(1) -else: +def main() -> int: + api_key = os.getenv("GEMINI_API_KEY") + if not api_key: + print("No API key found!") + return 1 + genai.configure(api_key=api_key) print("Available Models:") try: @@ -21,3 +22,8 @@ print(m.name) except Exception as e: print(f"Error listing models: {e}") + return 1 + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/backend/scripts/generate_book_mapping.py b/backend/scripts/generate_book_mapping.py index 479e4f2..aac5796 100644 --- a/backend/scripts/generate_book_mapping.py +++ b/backend/scripts/generate_book_mapping.py @@ -188,7 +188,11 @@ async def main(): except json.JSONDecodeError: mapping = [] - existing_files = {item["original_file"] for item in mapping} + existing_files = { + item.get("original_file") + for item in mapping + if isinstance(item, dict) and item.get("original_file") + } files_to_process = [f for f in txt_files if f.name not in existing_files] print(f"Skipping {len(existing_files)} already processed files. Processing {len(files_to_process)} new files.") From 0dd84a4eeb4dc9eb9926266b206f6bd0292fbfe6 Mon Sep 17 00:00:00 2001 From: Sanghun95 Date: Tue, 3 Mar 2026 10:43:17 +0900 Subject: [PATCH 05/52] refactor(backend): use shared env parser and HTTPS for API --- backend/app/core/env_utils.py | 35 ++++++++++++++++++++++++ backend/app/services/llm.py | 19 ++----------- backend/scripts/generate_book_mapping.py | 21 +++----------- 3 files changed, 41 insertions(+), 34 deletions(-) create mode 100644 backend/app/core/env_utils.py diff --git a/backend/app/core/env_utils.py b/backend/app/core/env_utils.py new file mode 100644 index 0000000..d169fa2 --- /dev/null +++ b/backend/app/core/env_utils.py @@ -0,0 +1,35 @@ +import os +import re +from pathlib import Path + +def parse_gemini_api_keys(env_path: Path) -> list[str]: + """ + Reads active GEMINI_API_KEY assignments from the given .env file. + Only extracts active assignments and strips inline comments and quotes. + Also falls back to os.environ if no keys are found in the file. + """ + api_keys = [] + + if env_path.exists(): + with open(env_path, 'r', encoding='utf-8') as f: + content = f.read() + # Find all variations of GEMINI_API_KEY assignments + matches = re.findall( + r'^\s*GEMINI_API_KEY\s*=\s*(.+?)\s*(?:#.*)?$', + content, + flags=re.MULTILINE, + ) + for m in matches: + # Remove inline comments and strip quotes + m = re.split(r'\s+#', m, 1)[0] + key = m.strip().strip('"').strip("'") + if key and key not in api_keys: + api_keys.append(key) + + # Fallback to os.environ when parsing produced no key or file doesn't exist + if not api_keys: + k = os.getenv("GEMINI_API_KEY") + if k and k not in api_keys: + api_keys.append(k) + + return api_keys diff --git a/backend/app/services/llm.py b/backend/app/services/llm.py index aa42417..c6183b2 100644 --- a/backend/app/services/llm.py +++ b/backend/app/services/llm.py @@ -4,6 +4,7 @@ from pathlib import Path import google.generativeai as genai from app.core.config import settings +from app.core.env_utils import parse_gemini_api_keys from langchain_core.prompts import PromptTemplate from langchain_google_genai import ChatGoogleGenerativeAI from langchain_core.output_parsers import StrOutputParser @@ -14,24 +15,8 @@ def get_all_gemini_keys() -> list[str]: """Reads active GEMINI_API_KEY assignments from the root .env file.""" - keys = [] env_path = Path(__file__).resolve().parents[3] / ".env" - - if env_path.exists(): - with open(env_path, "r", encoding="utf-8") as f: - content = f.read() - # Find all variations of GEMINI_API_KEY assignments - matches = re.findall( - r'^\s*GEMINI_API_KEY\s*=\s*(.+?)\s*(?:#.*)?$', - content, - flags=re.MULTILINE, - ) - for m in matches: - # Remove inline comments and strip quotes - m = re.split(r'\s+#', m, 1)[0] - key = m.strip().strip('"').strip("'") - if key and key not in keys: - keys.append(key) + keys = parse_gemini_api_keys(env_path) # Ensure the one from environment variables/settings is also included if getattr(settings, "GEMINI_API_KEY", None) and settings.GEMINI_API_KEY not in keys: diff --git a/backend/scripts/generate_book_mapping.py b/backend/scripts/generate_book_mapping.py index aac5796..5e115dc 100644 --- a/backend/scripts/generate_book_mapping.py +++ b/backend/scripts/generate_book_mapping.py @@ -15,27 +15,14 @@ if str(backend_dir) not in sys.path: sys.path.insert(0, str(backend_dir)) +from app.core.env_utils import parse_gemini_api_keys + env_path = backend_dir.parent / ".env" print(f"Loading .env from {env_path}, exists: {env_path.exists()}") load_dotenv(dotenv_path=env_path) # Extract ALL GEMINI_API_KEYs from .env file -api_keys = [] -if env_path.exists(): - with open(env_path, 'r', encoding='utf-8') as f: - for line in f: - m = re.match(r'^\s*GEMINI_API_KEY\s*=\s*(.+?)\s*(?:#.*)?$', line) - if not m: - continue - key = m.group(1).strip().strip('"').strip("'") - if key and key not in api_keys: - api_keys.append(key) - -# Fallback to os.environ when parsing produced no key -if not api_keys: - k = os.getenv("GEMINI_API_KEY") - if k and k not in api_keys: - api_keys.append(k) +api_keys = parse_gemini_api_keys(env_path) # The user explicitly asked to start testing from new keys (lines 8~14), and then go back to line 2. # Rotates keys only when running with ENABLE_TEST_KEY_ROTATION flag. @@ -129,7 +116,7 @@ async def search_aladin(title: str, author: str) -> dict: return {} query = f"{title} {author}".strip() - url = f"http://www.aladin.co.kr/ttb/api/ItemSearch.aspx?ttbkey={ALADIN_API_KEY}&Query={urllib.parse.quote(query)}&QueryType=Keyword&MaxResults=1&start=1&SearchTarget=Book&output=js&Version=20131101" + url = f"https://www.aladin.co.kr/ttb/api/ItemSearch.aspx?ttbkey={ALADIN_API_KEY}&Query={urllib.parse.quote(query)}&QueryType=Keyword&MaxResults=1&start=1&SearchTarget=Book&output=js&Version=20131101" try: loop = asyncio.get_running_loop() From 3057ad7f18bb234f6329c597dcb00c0c4f2e6832 Mon Sep 17 00:00:00 2001 From: Sanghun95 Date: Tue, 3 Mar 2026 10:52:47 +0900 Subject: [PATCH 06/52] fix(backend): allow key rotation for all errors in book mapping --- backend/scripts/generate_book_mapping.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/scripts/generate_book_mapping.py b/backend/scripts/generate_book_mapping.py index 5e115dc..6af9e45 100644 --- a/backend/scripts/generate_book_mapping.py +++ b/backend/scripts/generate_book_mapping.py @@ -99,12 +99,19 @@ async def translate_book_info(file_name: str) -> dict: current_key_idx += 1 except Exception as e: error_str = str(e).lower() - if "429" in error_str or "quota" in error_str or "exhausted" in error_str or "toomanyrequests" in error_str: - print(f"API Key {current_key_idx} exhausted. Switching to next key...") + should_rotate = any( + token in error_str + for token in ( + "429", "quota", "exhausted", "toomanyrequests", + "401", "403", "invalid api key", "permission", "unauthenticated", + ) + ) + if should_rotate: + print(f"API Key {current_key_idx} exhausted/invalid. Switching to next key...") current_key_idx += 1 else: print(f"Failed to parse LLM translation for {file_name}: {e}") - break + current_key_idx += 1 # If all keys exhausted or other error, fallback print(f"LLM Failed for {file_name}, falling back to Kyobo Search...") From fc24774b928502f148304ff8bf0d55d1d7ce1e30 Mon Sep 17 00:00:00 2001 From: Sanghun95 Date: Tue, 3 Mar 2026 15:05:22 +0900 Subject: [PATCH 07/52] feat: implement dynamic chat title and dynamic philosopher highlighting --- backend/app/api/routes/chat.py | 22 +++++- backend/app/services/llm.py | 16 +++++ frontend/app/page.tsx | 31 +++++++-- frontend/components/chat/ChatMain.tsx | 8 ++- frontend/components/chat/MessageList.tsx | 60 +++++++++++++++- .../components/sidebar/ActivePhilosophers.tsx | 69 ++++++++++++------- frontend/components/sidebar/Sidebar.tsx | 15 ++-- 7 files changed, 182 insertions(+), 39 deletions(-) diff --git a/backend/app/api/routes/chat.py b/backend/app/api/routes/chat.py index f200b95..0402ec8 100644 --- a/backend/app/api/routes/chat.py +++ b/backend/app/api/routes/chat.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field from sse_starlette.sse import EventSourceResponse -from app.services.llm import get_english_translation, get_response_stream_async +from app.services.llm import get_english_translation, get_response_stream_async, generate_chat_title_async from app.services.embedding import embedding_service from app.services.database import get_client from app.core.rate_limit import limiter @@ -22,6 +22,9 @@ class ChatRequest(BaseModel): query: str history: List[HistoryMessage] = Field(default_factory=list) +class TitleRequest(BaseModel): + query: str + def _search_documents(query_vector): return get_client().rpc( 'match_documents', @@ -127,3 +130,20 @@ async def chat_endpoint(request: Request, chat_request: ChatRequest): Endpoint for accepting chat queries and returning a text/event-stream response. """ return EventSourceResponse(generate_chat_events(request, chat_request.query, chat_request.history)) + +@router.post("/title") +@limiter.limit("5/minute") +async def chat_title_endpoint(request: Request, title_request: TitleRequest): + """ + Endpoint for generating a short chat room title based on the first user query. + """ + try: + title = await generate_chat_title_async(title_request.query) + # Handle case where LLM returns something too long or with quotes + title = title.replace('"', '').replace("'", "").strip() + if len(title) > 20: # Ensure it's short even if LLM disobeys a bit + title = title[:20] + "..." + return {"title": title} + except Exception: + logger.exception("Failed to generate chat title") + return {"title": "새로운 대화"} diff --git a/backend/app/services/llm.py b/backend/app/services/llm.py index c6183b2..6682f21 100644 --- a/backend/app/services/llm.py +++ b/backend/app/services/llm.py @@ -120,3 +120,19 @@ async def get_response_stream_async(context: str, query: str, history: str = "") chain = prompt | get_llm() | StrOutputParser() async for chunk in chain.astream({"context": context, "chat_history": history, "query": query}): yield chunk + +title_prompt = PromptTemplate.from_template( + """주어진 질문을 기반으로 철학적인 대화방 제목을 15자 이내로 지어줘. + 부연 설명 없이 제목만 출력해. + + 질문: {query} + 제목: """ +) + +async def generate_chat_title_async(query: str) -> str: + """ + Generates a short chat title based on the user's first query using Gemini. + """ + chain = title_prompt | get_llm() | StrOutputParser() + title = await chain.ainvoke({"query": query}) + return title.strip() diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 6d5a49a..22bb04f 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -3,12 +3,14 @@ import { useState, useCallback } from "react"; import { Sidebar } from "../components/sidebar/Sidebar"; import { ChatMain } from "../components/chat/ChatMain"; -import { Message } from "../types/chat"; +import { Message, DocumentMetadata } from "../types/chat"; export default function Home() { const [messages, setMessages] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const [chatTitle, setChatTitle] = useState("미덕에 관한 대화"); + const [activeMetadata, setActiveMetadata] = useState([]); const processLine = useCallback((line: string, eventObj: { current: string }, aiMsgId: string): boolean => { if (line.startsWith("event: ")) { @@ -67,13 +69,28 @@ export default function Home() { setMessages((prev) => [...prev, newUserMsg, placeholderAiMsg]); setIsSubmitting(true); + const isFirstMessage = messages.length === 0; + const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"; + + if (isFirstMessage) { + fetch(`${baseUrl}/api/v1/chat/title`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query: query }) + }) + .then(res => res.json()) + .then(data => { + if (data.title) setChatTitle(data.title); + }) + .catch(err => console.error("Failed to fetch title:", err)); + } + try { const historyToSend = messages.slice(-10).map(msg => ({ role: msg.role, content: msg.content })); - const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"; const res = await fetch(`${baseUrl}/api/v1/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -152,13 +169,19 @@ export default function Home() { return (
- setIsSidebarOpen(false)} /> + setIsSidebarOpen(false)} /> setMessages([])} + onClearChat={() => { + setMessages([]); + setChatTitle("미덕에 관한 대화"); + setActiveMetadata([]); + }} onMenuClick={() => setIsSidebarOpen(true)} + onVisibleMessageChange={setActiveMetadata} />
); diff --git a/frontend/components/chat/ChatMain.tsx b/frontend/components/chat/ChatMain.tsx index c0b87cb..9bb9ff4 100644 --- a/frontend/components/chat/ChatMain.tsx +++ b/frontend/components/chat/ChatMain.tsx @@ -8,13 +8,15 @@ import { Message } from "../../types/chat"; interface ChatMainProps { messages: Message[]; + chatTitle?: string; onSendMessage: (query: string) => void; isSubmitting: boolean; onClearChat: () => void; onMenuClick?: () => void; + onVisibleMessageChange?: (meta: DocumentMetadata[]) => void; } -export function ChatMain({ messages, onSendMessage, isSubmitting, onClearChat, onMenuClick }: ChatMainProps) { +export function ChatMain({ messages, chatTitle = "미덕에 관한 대화", onSendMessage, isSubmitting, onClearChat, onMenuClick, onVisibleMessageChange }: ChatMainProps) { const messagesEndRef = useRef(null); const [shouldAutoScroll, setShouldAutoScroll] = useState(true); const [startTime, setStartTime] = useState(""); @@ -51,7 +53,7 @@ export function ChatMain({ messages, onSendMessage, isSubmitting, onClearChat, o
-

미덕에 관한 대화

+

{chatTitle}

세션 시작: {mounted ? startTime : ""}

@@ -69,7 +71,7 @@ export function ChatMain({ messages, onSendMessage, isSubmitting, onClearChat, o {/* Scrollable Message Area */}
- +
diff --git a/frontend/components/chat/MessageList.tsx b/frontend/components/chat/MessageList.tsx index d71bebb..3909a99 100644 --- a/frontend/components/chat/MessageList.tsx +++ b/frontend/components/chat/MessageList.tsx @@ -1,4 +1,5 @@ import { Sparkles, SquareArrowOutUpRight, ThumbsUp, Copy, RotateCcw } from "lucide-react"; +import { useEffect, useRef } from "react"; import { Message, DocumentMetadata } from "../../types/chat"; const DUMMY_COVER_URL = "https://image.aladin.co.kr/product/dummy"; @@ -7,9 +8,10 @@ const DUMMY_BOOK_LINK = "https://www.aladin.co.kr/dummy-link"; interface Props { messages: Message[]; onOpenCitation?: (meta: DocumentMetadata) => void; + onVisibleMessageChange?: (meta: DocumentMetadata[]) => void; } -export function MessageList({ messages, onOpenCitation }: Props) { +export function MessageList({ messages, onOpenCitation, onVisibleMessageChange }: Props) { if (messages.length === 0) { return (
@@ -24,6 +26,60 @@ export function MessageList({ messages, onOpenCitation }: Props) { ); } + const observer = useRef(null); + const visibleMessages = useRef>(new Map()); + + useEffect(() => { + if (!onVisibleMessageChange) return; + + observer.current = new IntersectionObserver((entries) => { + let changed = false; + entries.forEach(entry => { + const id = entry.target.getAttribute("data-message-id"); + if (id) { + if (entry.isIntersecting) { + visibleMessages.current.set(id, entry.intersectionRatio); + } else { + visibleMessages.current.delete(id); + } + changed = true; + } + }); + + if (changed) { + let maxRatio = -1; + let mostVisibleId: string | null = null; + visibleMessages.current.forEach((ratio, id) => { + if (ratio > maxRatio) { + maxRatio = ratio; + mostVisibleId = id; + } + }); + + if (mostVisibleId) { + const msg = messages.find(m => m.id === mostVisibleId); + if (msg && msg.metadata && msg.metadata.length > 0) { + onVisibleMessageChange(msg.metadata); + } + } else { + const aiMessages = messages.filter(m => m.role === "ai" && m.metadata && m.metadata.length > 0); + if (aiMessages.length > 0) { + onVisibleMessageChange(aiMessages[aiMessages.length - 1].metadata!); + } + } + } + }, { + threshold: [0, 0.25, 0.5, 0.75, 1.0] + }); + + const elements = document.querySelectorAll(".ai-message-card"); + elements.forEach(el => observer.current?.observe(el)); + + return () => { + observer.current?.disconnect(); + }; + }, [messages, onVisibleMessageChange]); + return (
@@ -40,7 +96,7 @@ export function MessageList({ messages, onOpenCitation }: Props) {
) : ( -
+
diff --git a/frontend/components/sidebar/ActivePhilosophers.tsx b/frontend/components/sidebar/ActivePhilosophers.tsx index 10ea5b0..9b2410f 100644 --- a/frontend/components/sidebar/ActivePhilosophers.tsx +++ b/frontend/components/sidebar/ActivePhilosophers.tsx @@ -2,48 +2,67 @@ import { BrainCircuit, CheckCircle } from "lucide-react"; import { DocumentMetadata } from "../../types/chat"; interface Props { - metadata: DocumentMetadata[]; + metadata: DocumentMetadata[]; // all seen philosophers + activeMetadata?: DocumentMetadata[]; // active philosophers onPhilosopherClick?: (scholar: string) => void; } -export function ActivePhilosophers({ metadata, onPhilosopherClick }: Props) { +export function ActivePhilosophers({ metadata, activeMetadata = [], onPhilosopherClick }: Props) { const uniquePhilosophers = Array.from(new Set(metadata.map(m => m.scholar))) .map(scholar => metadata.find(m => m.scholar === scholar)) .filter((m): m is DocumentMetadata => m !== undefined); + const activeScholarSet = new Set(activeMetadata.map(m => m.scholar)); + + // Sort to put highlighted philosophers at the top + const sortedPhilosophers = [...uniquePhilosophers].sort((a, b) => { + const aActive = activeScholarSet.has(a.scholar); + const bActive = activeScholarSet.has(b.scholar); + if (aActive && !bActive) return -1; + if (!aActive && bActive) return 1; + return 0; + }); + return (

활성화된 철학자

- {uniquePhilosophers.length === 0 ? ( + {sortedPhilosophers.length === 0 ? (

현재 참조 중인 철학자가 없습니다.

) : (
- {uniquePhilosophers.map((meta) => ( -
- - ))} + + ); + })}
)}
diff --git a/frontend/components/sidebar/Sidebar.tsx b/frontend/components/sidebar/Sidebar.tsx index 786fece..9b2a8d6 100644 --- a/frontend/components/sidebar/Sidebar.tsx +++ b/frontend/components/sidebar/Sidebar.tsx @@ -1,18 +1,25 @@ import { Settings, History, User, X } from "lucide-react"; import { ActivePhilosophers } from "./ActivePhilosophers"; import { ContextSources } from "./ContextSources"; -import { Message } from "../../types/chat"; +import { Message, DocumentMetadata } from "../../types/chat"; interface SidebarProps { messages?: Message[]; + activeMetadata?: DocumentMetadata[]; isOpen?: boolean; onClose?: () => void; } -export function Sidebar({ messages = [], isOpen = false, onClose }: SidebarProps) { +export function Sidebar({ messages = [], activeMetadata = [], isOpen = false, onClose }: SidebarProps) { const aiMessages = messages.filter(m => m.role === "ai" && m.metadata && m.metadata.length > 0); const currentMetadata = aiMessages.length > 0 ? aiMessages[aiMessages.length - 1].metadata! : []; + // All unique metadata observed throughout the chat so far + const allMetadata = aiMessages.flatMap(m => m.metadata || []); + + // Use active metadata from scroll if available, otherwise use latest message's metadata + const displayMetadata = activeMetadata.length > 0 ? activeMetadata : currentMetadata; + return ( <> {/* Mobile Overlay */} @@ -46,8 +53,8 @@ export function Sidebar({ messages = [], isOpen = false, onClose }: SidebarProps {/* Scrollable Content */}
- { /* TODO: Implement philosopher filter using scholar */ }} /> - + { /* TODO: Implement philosopher filter using scholar */ }} /> +
{/* System Status */} From cdbc817c67d7ba3fc8630c747eb313f4f03fc8ab Mon Sep 17 00:00:00 2001 From: Sanghun95 Date: Tue, 3 Mar 2026 15:31:42 +0900 Subject: [PATCH 08/52] fix: apply CodeRabbit PR review feedback --- backend/app/api/routes/chat.py | 9 +- backend/data/books_mapping.json | 134 +++++++++++------------ backend/scripts/generate_book_mapping.py | 4 +- backend/scripts/update_db_metadata.py | 4 + backend/update_metadata.sql | 4 +- frontend/components/chat/ChatMain.tsx | 2 +- frontend/components/chat/MessageList.tsx | 28 ++--- frontend/components/sidebar/Sidebar.tsx | 30 ++++- 8 files changed, 124 insertions(+), 91 deletions(-) diff --git a/backend/app/api/routes/chat.py b/backend/app/api/routes/chat.py index 0402ec8..7155c88 100644 --- a/backend/app/api/routes/chat.py +++ b/backend/app/api/routes/chat.py @@ -137,13 +137,20 @@ async def chat_title_endpoint(request: Request, title_request: TitleRequest): """ Endpoint for generating a short chat room title based on the first user query. """ + query = title_request.query.strip() + if not query: + return {"title": "새로운 대화"} + try: - title = await generate_chat_title_async(title_request.query) + title = await asyncio.wait_for(generate_chat_title_async(query), timeout=10.0) # Handle case where LLM returns something too long or with quotes title = title.replace('"', '').replace("'", "").strip() if len(title) > 20: # Ensure it's short even if LLM disobeys a bit title = title[:20] + "..." return {"title": title} + except asyncio.TimeoutError: + logger.warning(f"Timeout generating chat title for query: {query}") + return {"title": "새로운 대화"} except Exception: logger.exception("Failed to generate chat title") return {"title": "새로운 대화"} diff --git a/backend/data/books_mapping.json b/backend/data/books_mapping.json index 7bd9fc8..05afbfc 100644 --- a/backend/data/books_mapping.json +++ b/backend/data/books_mapping.json @@ -17,7 +17,7 @@ "translated_author": "데이비드 흄", "aladin_data": { "title": "인간이란 무엇인가 - 오성 정념 도덕 본성론", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=4359030&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=4359030&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/435/90/coversum/8949705206_1.jpg", "author": "데이비드 흄 (지은이), 김성숙 (옮긴이)", "isbn": "9788949705200" @@ -29,7 +29,7 @@ "translated_author": "메리 울스턴크래프트", "aladin_data": { "title": "여권의 옹호", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=45690064&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=45690064&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/4569/0/coversum/8994054596_1.jpg", "author": "메리 울스턴크래프트 (지은이), 손영미 (옮긴이)", "isbn": "9788994054599" @@ -41,7 +41,7 @@ "translated_author": "프리드리히 니체", "aladin_data": { "title": "차라투스트라는 이렇게 말했다", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=454014&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=454014&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/45/40/coversum/s352934786_1.jpg", "author": "프리드리히 니체 (지은이), 장희창 (옮긴이)", "isbn": "9788937460944" @@ -59,7 +59,7 @@ "translated_author": "존 로크", "aladin_data": { "title": "존 로크의 인간 오성론 읽기", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=90592125&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=90592125&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/9059/21/coversum/k092535101_1.jpg", "author": "안병웅 (지은이)", "isbn": "9791185136318" @@ -71,7 +71,7 @@ "translated_author": "플라톤", "aladin_data": { "title": "소크라테스의 변명 - 크리톤 파이돈 향연, 문예교양선서 30", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=224035&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=224035&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/22/40/coversum/8931003714_3.jpg", "author": "플라톤 (지은이), 황문수 (옮긴이)", "isbn": "9788931003710" @@ -83,7 +83,7 @@ "translated_author": "플라톤", "aladin_data": { "title": "소크라테스의 변명·크리톤·파이돈·향연 (그리스어 원전 완역본) - 플라톤의 대화편", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=216792703&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=216792703&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/21679/27/coversum/k252636705_1.jpg", "author": "플라톤 (지은이), 박문재 (옮긴이)", "isbn": "9791190398039" @@ -95,7 +95,7 @@ "translated_author": "제임스 앨런", "aladin_data": { "title": "제임스 앨런 원인과 결과의 법칙 - 사람은 생각하는 대로 살게 된다", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=345588057&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=345588057&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/34558/80/coversum/k732933167_1.jpg", "author": "제임스 알렌 (지은이), 박선영 (옮긴이)", "isbn": "9791171177660" @@ -113,7 +113,7 @@ "translated_author": "프리드리히 니체", "aladin_data": { "title": "선악의 저편", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=174923171&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=174923171&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/17492/31/coversum/8957336117_1.jpg", "author": "프리드리히 니체 (지은이), 박찬국 (옮긴이)", "isbn": "9788957336113" @@ -125,7 +125,7 @@ "translated_author": "마르쿠스 툴리우스 키케로", "aladin_data": { "title": "투스쿨룸 대화", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=286336783&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=286336783&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/28633/67/coversum/8957337679_1.jpg", "author": "마르쿠스 툴리우스 키케로 (지은이), 김남우 (옮긴이)", "isbn": "9788957337677" @@ -137,7 +137,7 @@ "translated_author": "마르쿠스 툴리우스 키케로", "aladin_data": { "title": "키케로의 의무론 - 그의 아들에게 보낸 편지", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=852420&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=852420&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/85/24/coversum/8930606245_1.jpg", "author": "마르쿠스 툴리우스 키케로 (지은이), 허승일 (옮긴이)", "isbn": "9788930606240" @@ -149,7 +149,7 @@ "translated_author": "존 듀이", "aladin_data": { "title": "민주주의와 교육", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=922961&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=922961&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/92/29/coversum/8925400669_2.jpg", "author": "존 듀이 (지은이), 이홍우 (옮긴이)", "isbn": "9788925400662" @@ -173,7 +173,7 @@ "translated_author": "르네 데카르트", "aladin_data": { "title": "방법서설 - 이성을 잘 인도하고 학문에서 진리를 찾기 위한", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=347983217&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=347983217&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/34798/32/coversum/k152933225_1.jpg", "author": "르네 데카르트 (지은이), 이재훈 (옮긴이)", "isbn": "9791170872443" @@ -185,7 +185,7 @@ "translated_author": "프리드리히 니체", "aladin_data": { "title": "이 사람을 보라", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=302356844&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=302356844&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/30235/68/coversum/8957338195_1.jpg", "author": "프리드리히 니체 (지은이), 박찬국 (옮긴이)", "isbn": "9788957338193" @@ -197,7 +197,7 @@ "translated_author": "랠프 월도 에머슨", "aladin_data": { "title": "에머슨 수상록", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=100408&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=100408&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/10/4/coversum/8972432954_2.jpg", "author": "랄프 왈도 에머슨 (지은이)", "isbn": "9788972432951" @@ -209,7 +209,7 @@ "translated_author": "아르투어 쇼펜하우어", "aladin_data": { "title": "쇼펜하우어의 행복론과 인생론", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=308665960&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=308665960&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/30866/59/coversum/8932440093_1.jpg", "author": "아르투어 쇼펜하우어 (지은이), 홍성광 (옮긴이)", "isbn": "9788932440095" @@ -221,7 +221,7 @@ "translated_author": "베네딕투스 데 스피노자", "aladin_data": { "title": "에티카 - 개정판", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=993377&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=993377&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/99/33/coversum/8930625460_2.jpg", "author": "베네딕투스 데 스피노자 (지은이), 강영계 (옮긴이)", "isbn": "9788930625463" @@ -239,7 +239,7 @@ "translated_author": "플라톤", "aladin_data": { "title": "에우튀프론", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=272171162&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=272171162&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/27217/11/coversum/8957337342_1.jpg", "author": "플라톤 (지은이), 강성훈 (옮긴이)", "isbn": "9788957337349" @@ -251,7 +251,7 @@ "translated_author": "임마누엘 칸트", "aladin_data": { "title": "윤리형이상학 정초 - 개정2판", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=168355651&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=168355651&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/16835/56/coversum/8957336036_1.jpg", "author": "임마누엘 칸트 (지은이), 백종현 (옮긴이)", "isbn": "9788957336038" @@ -269,7 +269,7 @@ "translated_author": "플라톤", "aladin_data": { "title": "고르기아스", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=265344583&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=265344583&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/26534/45/coversum/8957337210_1.jpg", "author": "플라톤 (지은이), 김인곤 (옮긴이)", "isbn": "9788957337219" @@ -287,7 +287,7 @@ "translated_author": "프리드리히 니체", "aladin_data": { "title": "인간적인 너무나 인간적인 1", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=279599&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=279599&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/27/95/coversum/8970132619_3.jpg", "author": "프리드리히 니체 (지은이), 김미기 (옮긴이)", "isbn": "9788970132617" @@ -305,7 +305,7 @@ "translated_author": "플라톤", "aladin_data": { "title": "플라톤의 법률", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=4646467&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=4646467&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/464/64/coversum/8930606296_1.jpg", "author": "플라톤 (지은이), 박종현 (옮긴이)", "isbn": "9788930606295" @@ -317,7 +317,7 @@ "translated_author": "토마스 홉스", "aladin_data": { "title": "리바이어던 1 - 교회국가 및 시민국가의 재료와 형태 및 권력", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=2480851&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=2480851&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/248/8/coversum/s392037901_2.jpg", "author": "토마스 홉스 (지은이), 진석용 (옮긴이)", "isbn": "9788930083379" @@ -329,7 +329,7 @@ "translated_author": "마르쿠스 아우렐리우스", "aladin_data": { "title": "명상록 - 삶과 죽음을 고뇌한 어느 철학자 황제의 가장 사적인 기록", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=384592083&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=384592083&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/38459/20/coversum/k062135812_1.jpg", "author": "마르쿠스 아우렐리우스 (지은이), 정미화 (옮긴이), 그레고리 헤이스 (해제)", "isbn": "9791168273993" @@ -341,7 +341,7 @@ "translated_author": "랄프 왈도 에머슨", "aladin_data": { "title": "랄프 왈도 에머슨 : 자연", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=39251790&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=39251790&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/3925/17/coversum/8956607648_1.jpg", "author": "랄프 왈도 에머슨 (지은이), 서동석 (옮긴이)", "isbn": "9788956607641" @@ -353,7 +353,7 @@ "translated_author": "토마스 칼라일", "aladin_data": { "title": "영웅숭배론", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=313531822&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=313531822&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/31353/18/coversum/8935678147_1.jpg", "author": "토머스 칼라일 (지은이), 박상익 (옮긴이)", "isbn": "9788935678143" @@ -365,7 +365,7 @@ "translated_author": "존 스튜어트 밀", "aladin_data": { "title": "초판본 자유론 - 1859년 오리지널 초판본 표지 디자인", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=381938135&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=381938135&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/38193/81/coversum/k302034718_1.jpg", "author": "존 스튜어트 밀 (지은이), 김희상 (옮긴이)", "isbn": "9791175241961" @@ -377,7 +377,7 @@ "translated_author": "헨리 데이비드 소로", "aladin_data": { "title": "월든·시민 불복종 (합본 완역본)", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=284194464&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=284194464&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/28419/44/coversum/k742835213_1.jpg", "author": "헨리 데이비드 소로 (지은이), 이종인 (옮긴이), 허버트 웬델 글리슨 (사진)", "isbn": "9791139700503" @@ -389,7 +389,7 @@ "translated_author": "루크레티우스", "aladin_data": { "title": "사물의 본성에 관하여", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=14599483&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=14599483&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/1459/94/coversum/8957332227_1.jpg", "author": "루크레티우스 (지은이), 강대진 (옮긴이)", "isbn": "9788957332221" @@ -401,7 +401,7 @@ "translated_author": "카를 폰 클라우제비츠", "aladin_data": { "title": "전쟁론 - 전면완역개정판", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=86524117&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=86524117&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/8652/41/coversum/8961951424_1.jpg", "author": "카알 폰 클라우제비츠 (지은이), 김만수 (옮긴이)", "isbn": "9788961951425" @@ -413,7 +413,7 @@ "translated_author": "블레즈 파스칼", "aladin_data": { "title": "파스칼의 팡세", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=367576319&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=367576319&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/36757/63/coversum/k952030294_1.jpg", "author": "블레즈 파스칼 (지은이), 강현규 (엮은이), 이선미 (옮긴이)", "isbn": "9791160029529" @@ -425,7 +425,7 @@ "translated_author": "임마누엘 칸트", "aladin_data": { "title": "영구 평화론 - 하나의 철학적 기획, 개정판", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=2881780&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=2881780&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/288/17/coversum/8930610439_1.jpg", "author": "임마누엘 칸트 (지은이), 이한구 (옮긴이)", "isbn": "9788930610438" @@ -437,7 +437,7 @@ "translated_author": "플라톤", "aladin_data": { "title": "소크라테스의 변명·크리톤·파이돈·향연 (그리스어 원전 완역본) - 플라톤의 대화편", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=216792703&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=216792703&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/21679/27/coversum/k252636705_1.jpg", "author": "플라톤 (지은이), 박문재 (옮긴이)", "isbn": "9791190398039" @@ -449,7 +449,7 @@ "translated_author": "플라톤", "aladin_data": { "title": "파이드로스 - 그리스어 원전 번역판", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=1820615&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=1820615&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/182/6/coversum/8931005881_2.jpg", "author": "플라톤 (지은이), 조대호 (옮긴이)", "isbn": "9788931005882" @@ -467,7 +467,7 @@ "translated_author": "플루타르코스", "aladin_data": { "title": "플루타르코스 영웅전", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=6970308&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=6970308&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/697/3/coversum/8991290337_2.jpg", "author": "플루타르코스 (지은이), 천병희 (옮긴이)", "isbn": "9788991290334" @@ -479,7 +479,7 @@ "translated_author": "아리스토텔레스", "aladin_data": { "title": "정치학", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=4399813&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=4399813&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/439/98/coversum/8991290280_1.jpg", "author": "아리스토텔레스 (지은이), 천병희 (옮긴이)", "isbn": "9788991290280" @@ -503,7 +503,7 @@ "translated_author": "카를 구스타프 융", "aladin_data": { "title": "칼 융 무의식의 심리학", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=300205010&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=300205010&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/30020/50/coversum/k222838355_1.jpg", "author": "칼 구스타프 융 (지은이), 정명진 (옮긴이)", "isbn": "9791159201479" @@ -533,7 +533,7 @@ "translated_author": "존 로크", "aladin_data": { "title": "통치론 - 시민정부의 참된 기원, 범위 및 그 목적에 관한 시론", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=301106377&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=301106377&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/30110/63/coversum/897291780x_1.jpg", "author": "존 로크 (지은이), 강정인, 문지영 (옮긴이)", "isbn": "9788972917809" @@ -545,7 +545,7 @@ "translated_author": "헤르만 헤세", "aladin_data": { "title": "싯다르타", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=329596&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=329596&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/32/95/coversum/s062934786_1.jpg", "author": "헤르만 헤세 (지은이), 박병덕 (옮긴이)", "isbn": "9788937460586" @@ -557,7 +557,7 @@ "translated_author": "손무", "aladin_data": { "title": "손자병법 - 이겨놓고 싸우는 인생의 지혜", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=372980631&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=372980631&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/37298/6/coversum/k292031545_1.jpg", "author": "손무 (지은이), 소준섭 (옮긴이)", "isbn": "9791139728002" @@ -569,7 +569,7 @@ "translated_author": "플라톤", "aladin_data": { "title": "소크라테스의 변명·크리톤·파이돈·향연 (그리스어 원전 완역본) - 플라톤의 대화편", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=216792703&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=216792703&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/21679/27/coversum/k252636705_1.jpg", "author": "플라톤 (지은이), 박문재 (옮긴이)", "isbn": "9791190398039" @@ -587,7 +587,7 @@ "translated_author": "프리드리히 빌헬름 니체", "aladin_data": { "title": "안티크리스트", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=35425033&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=35425033&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/3542/50/coversum/8957333444_1.jpg", "author": "프리드리히 니체 (지은이), 박찬국 (옮긴이)", "isbn": "9788957333440" @@ -599,7 +599,7 @@ "translated_author": "프리드리히 빌헬름 니체", "aladin_data": { "title": "비극의 탄생", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=988511&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=988511&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/98/85/coversum/8957331077_1.jpg", "author": "프리드리히 니체 (지은이), 박찬국 (옮긴이)", "isbn": "9788957331071" @@ -611,7 +611,7 @@ "translated_author": "프리드리히 니체", "aladin_data": { "title": "우상의 황혼", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=64193963&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=64193963&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/6419/39/coversum/8957334513_1.jpg", "author": "프리드리히 니체 (지은이), 박찬국 (옮긴이)", "isbn": "9788957334515" @@ -635,7 +635,7 @@ "translated_author": "카를 마르크스, 프리드리히 엥겔스", "aladin_data": { "title": "공산당 선언", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=143257420&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=143257420&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/14325/74/coversum/k172532941_1.jpg", "author": "카를 마르크스, 프리드리히 엥겔스 (지은이), 심철민 (옮긴이)", "isbn": "9791187036548" @@ -653,7 +653,7 @@ "translated_author": "보에티우스", "aladin_data": { "title": "철학의 위안 - 완역본", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=147121964&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=147121964&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/14712/19/coversum/k002532053_2.jpg", "author": "아니키우스 보이티우스 (지은이), 박문재 (옮긴이)", "isbn": "9791187142430" @@ -665,7 +665,7 @@ "translated_author": "이마누엘 칸트", "aladin_data": { "title": "순수이성비판 1", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=669748&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=669748&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/66/97/coversum/8957330836_1.jpg", "author": "임마누엘 칸트 (지은이), 백종현 (옮긴이)", "isbn": "9788957330838" @@ -677,7 +677,7 @@ "translated_author": "에픽테토스", "aladin_data": { "title": "[POD] 에픽테토스 편람 (스토아 사상 철학자) : The Enchiridion (영어원서)", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=231320657&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=231320657&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/23132/6/coversum/k022637708_1.jpg", "author": "에픽테토스 (지은이)", "isbn": "9791127295479" @@ -695,7 +695,7 @@ "translated_author": "아르투어 쇼펜하우어", "aladin_data": { "title": "쇼펜하우어의 행복론과 인생론", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=308665960&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=308665960&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/30866/59/coversum/8932440093_1.jpg", "author": "아르투어 쇼펜하우어 (지은이), 홍성광 (옮긴이)", "isbn": "9788932440095" @@ -707,7 +707,7 @@ "translated_author": "아리스토텔레스", "aladin_data": { "title": "니코마코스 윤리학 - 그리스어 원전 번역, 개정판", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=31685631&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=31685631&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/3168/56/coversum/8991290523_3.jpg", "author": "아리스토텔레스 (지은이), 천병희 (옮긴이)", "isbn": "9788991290525" @@ -719,7 +719,7 @@ "translated_author": "프리드리히 니체", "aladin_data": { "title": "도덕의 계보", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=274647853&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=274647853&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/27464/78/coversum/8957337350_1.jpg", "author": "프리드리히 니체 (지은이), 박찬국 (옮긴이)", "isbn": "9788957337356" @@ -761,7 +761,7 @@ "translated_author": "윌리엄 블레이크", "aladin_data": { "title": "천국과 지옥의 결혼", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=141182&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=141182&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/14/11/coversum/8937418460_2.jpg", "author": "윌리엄 블레이크 (지은이), 김종철 (옮긴이)", "isbn": "9788937418464" @@ -773,7 +773,7 @@ "translated_author": "마르쿠스 아우렐리우스", "aladin_data": { "title": "명상록 - 삶과 죽음을 고뇌한 어느 철학자 황제의 가장 사적인 기록", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=384592083&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=384592083&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/38459/20/coversum/k062135812_1.jpg", "author": "마르쿠스 아우렐리우스 (지은이), 정미화 (옮긴이), 그레고리 헤이스 (해제)", "isbn": "9791168273993" @@ -785,7 +785,7 @@ "translated_author": "아리스토텔레스", "aladin_data": { "title": "아리스토텔레스 시학 (그리스어 원전 완역본)", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=265596201&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=265596201&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/26559/62/coversum/k392738937_1.jpg", "author": "아리스토텔레스 (지은이), 박문재 (옮긴이)", "isbn": "9791166812453" @@ -797,7 +797,7 @@ "translated_author": "니콜로 마키아벨리", "aladin_data": { "title": "초판본 군주론 - 오리지널 초판본 표지디자인", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=249432298&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=249432298&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/24943/22/coversum/k032632692_2.jpg", "author": "니콜로 마키아벨리 (지은이), 이시연 (옮긴이)", "isbn": "9791164453085" @@ -815,7 +815,7 @@ "translated_author": "버트런드 러셀", "aladin_data": { "title": "철학의 문제들 - 전면 개역판", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=385198660&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=385198660&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/38519/86/coversum/8961474928_1.jpg", "author": "버트런드 러셀 (지은이), 박영태 (옮긴이)", "isbn": "9788961474924" @@ -827,7 +827,7 @@ "translated_author": "칼릴 지브란", "aladin_data": { "title": "예언자", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=129499645&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=129499645&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/12949/96/coversum/k672532485_1.jpg", "author": "칼릴 지브란 (지은이), 류시화 (옮긴이)", "isbn": "9791186686294" @@ -839,7 +839,7 @@ "translated_author": "플라톤", "aladin_data": { "title": "플라톤의 국가·정체(政體) - 개정 증보판", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=16812&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=16812&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/1/68/coversum/8930606237_2.jpg", "author": "플라톤 (지은이), 박종현 (옮긴이)", "isbn": "9788930606233" @@ -851,7 +851,7 @@ "translated_author": "플라톤", "aladin_data": { "title": "플라톤의 국가·정체(政體) - 개정 증보판", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=16812&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=16812&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/1/68/coversum/8930606237_2.jpg", "author": "플라톤 (지은이), 박종현 (옮긴이)", "isbn": "9788930606233" @@ -875,7 +875,7 @@ "translated_author": "장 자크 루소", "aladin_data": { "title": "사회계약론 - 자유와 평등을 위한 약속", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=139172090&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=139172090&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/13917/20/coversum/8961672398_1.jpg", "author": "장 자크 루소 (지은이), 권혁 (옮긴이)", "isbn": "9788961672399" @@ -911,7 +911,7 @@ "translated_author": "아르투어 쇼펜하우어", "aladin_data": { "title": "의지와 표상으로서의 세계", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=95607072&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=95607072&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/9560/70/coversum/8949714221_2.jpg", "author": "아르투어 쇼펜하우어 (지은이), 권기철 (옮긴이)", "isbn": "9788949714226" @@ -923,7 +923,7 @@ "translated_author": "프리드리히 니체", "aladin_data": { "title": "차라투스트라는 이렇게 말했다", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=454014&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=454014&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/45/40/coversum/s352934786_1.jpg", "author": "프리드리히 니체 (지은이), 장희창 (옮긴이)", "isbn": "9788937460944" @@ -935,7 +935,7 @@ "translated_author": "존 스튜어트 밀", "aladin_data": { "title": "공리주의", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=243048009&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=243048009&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/24304/80/coversum/k452630592_1.jpg", "author": "존 스튜어트 밀 (지은이), 이종인 (옮긴이)", "isbn": "9791190878142" @@ -947,7 +947,7 @@ "translated_author": "토머스 모어", "aladin_data": { "title": "유토피아", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=565805&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=565805&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/56/58/coversum/8974832534_1.jpg", "author": "토머스 모어 (지은이), 나종일 (옮긴이)", "isbn": "9788974832537" @@ -959,7 +959,7 @@ "translated_author": "헨리 데이비드 소로", "aladin_data": { "title": "월든 - 완결판", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=12840843&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=12840843&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/1284/8/coversum/8956605416_3.jpg", "author": "헨리 데이비드 소로 (지은이), 강승영 (옮긴이)", "isbn": "9788956605418" @@ -971,7 +971,7 @@ "translated_author": "레프 톨스토이", "aladin_data": { "title": "예술이란 무엇인가", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=319767632&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=319767632&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/31976/76/coversum/k312834367_1.jpg", "author": "레프 니콜라예비치 톨스토이 (지은이), 이강은 (옮긴이)", "isbn": "9791166891694" @@ -983,7 +983,7 @@ "translated_author": "유향", "aladin_data": { "title": "신서 1", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=6171917&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=6171917&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/617/19/coversum/8949705818_1.jpg", "author": "유향 (지은이), 임동석 (옮긴이)", "isbn": "9788949705811" @@ -995,7 +995,7 @@ "translated_author": "고염무", "aladin_data": { "title": "원서발췌 일지록", - "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=383905503&partner=openAPI&start=api", + "link": "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=383905503&partner=openAPI&start=api", "thumbnail": "https://image.aladin.co.kr/product/38390/55/coversum/k522135566_1.jpg", "author": "고염무 (지은이), 윤대식 (옮긴이)", "isbn": "9791143017086" diff --git a/backend/scripts/generate_book_mapping.py b/backend/scripts/generate_book_mapping.py index 6af9e45..424b2e7 100644 --- a/backend/scripts/generate_book_mapping.py +++ b/backend/scripts/generate_book_mapping.py @@ -111,7 +111,7 @@ async def translate_book_info(file_name: str) -> dict: current_key_idx += 1 else: print(f"Failed to parse LLM translation for {file_name}: {e}") - current_key_idx += 1 + break # If all keys exhausted or other error, fallback print(f"LLM Failed for {file_name}, falling back to Kyobo Search...") @@ -139,7 +139,7 @@ def fetch(): item = items[0] return { "title": item.get("title", ""), - "link": item.get("link", ""), + "link": item.get("link", "").replace("&", "&"), "thumbnail": item.get("cover", ""), "author": item.get("author", ""), "isbn": item.get("isbn13", "") diff --git a/backend/scripts/update_db_metadata.py b/backend/scripts/update_db_metadata.py index bd77027..434a721 100644 --- a/backend/scripts/update_db_metadata.py +++ b/backend/scripts/update_db_metadata.py @@ -85,6 +85,10 @@ def update_database(): doc_id = doc['id'] metadata = doc['metadata'] + if not isinstance(metadata, dict): + print(f"Skipping doc {doc_id}: metadata is not a dict") + continue + # The DB stores the title we want to match inside metadata->book_info->title db_title = metadata.get('book_info', {}).get('title', '') diff --git a/backend/update_metadata.sql b/backend/update_metadata.sql index 51b2a24..0cf0877 100644 --- a/backend/update_metadata.sql +++ b/backend/update_metadata.sql @@ -1,10 +1,10 @@ CREATE INDEX IF NOT EXISTS idx_documents_book_title ON documents ((metadata->'book_info'->>'title')); -SET statement_timeout = '120s'; -- Increase timeout to be safe - BEGIN; +SET LOCAL statement_timeout = '120s'; -- Increase timeout to be safe for this transaction + UPDATE documents SET metadata = metadata || '{"kr_title": "역설의 예산 1권", "thumbnail": "", "link": ""}'::jsonb WHERE metadata->'book_info'->>'title' = 'Korean Translation of A Budget of Paradoxes Volume I'; diff --git a/frontend/components/chat/ChatMain.tsx b/frontend/components/chat/ChatMain.tsx index 9bb9ff4..c9479bd 100644 --- a/frontend/components/chat/ChatMain.tsx +++ b/frontend/components/chat/ChatMain.tsx @@ -4,7 +4,7 @@ import { Share, Plus, Menu } from "lucide-react"; import { useRef, useEffect, useState } from "react"; import { MessageList } from "./MessageList"; import { FloatingInput } from "./FloatingInput"; -import { Message } from "../../types/chat"; +import { Message, DocumentMetadata } from "../../types/chat"; interface ChatMainProps { messages: Message[]; diff --git a/frontend/components/chat/MessageList.tsx b/frontend/components/chat/MessageList.tsx index 3909a99..225dc8a 100644 --- a/frontend/components/chat/MessageList.tsx +++ b/frontend/components/chat/MessageList.tsx @@ -12,20 +12,6 @@ interface Props { } export function MessageList({ messages, onOpenCitation, onVisibleMessageChange }: Props) { - if (messages.length === 0) { - return ( -
-
- -
-

어떤 철학적 고민이 있으신가요?

-

- 미덕, 죽음, 사랑, 자아 등 삶의 본질적인 질문들을 과거의 위대한 철학자들과 함께 탐구해보세요. -

-
- ); - } - const observer = useRef(null); const visibleMessages = useRef>(new Map()); @@ -80,6 +66,20 @@ export function MessageList({ messages, onOpenCitation, onVisibleMessageChange } }; }, [messages, onVisibleMessageChange]); + if (messages.length === 0) { + return ( +
+
+ +
+

어떤 철학적 고민이 있으신가요?

+

+ 미덕, 죽음, 사랑, 자아 등 삶의 본질적인 질문들을 과거의 위대한 철학자들과 함께 탐구해보세요. +

+
+ ); + } + return (
diff --git a/frontend/components/sidebar/Sidebar.tsx b/frontend/components/sidebar/Sidebar.tsx index 9b2a8d6..8c088d0 100644 --- a/frontend/components/sidebar/Sidebar.tsx +++ b/frontend/components/sidebar/Sidebar.tsx @@ -1,4 +1,5 @@ import { Settings, History, User, X } from "lucide-react"; +import { useState } from "react"; import { ActivePhilosophers } from "./ActivePhilosophers"; import { ContextSources } from "./ContextSources"; import { Message, DocumentMetadata } from "../../types/chat"; @@ -11,14 +12,35 @@ interface SidebarProps { } export function Sidebar({ messages = [], activeMetadata = [], isOpen = false, onClose }: SidebarProps) { + const [filterScholar, setFilterScholar] = useState(null); + const aiMessages = messages.filter(m => m.role === "ai" && m.metadata && m.metadata.length > 0); const currentMetadata = aiMessages.length > 0 ? aiMessages[aiMessages.length - 1].metadata! : []; - // All unique metadata observed throughout the chat so far - const allMetadata = aiMessages.flatMap(m => m.metadata || []); + // All unique metadata observed throughout the chat so far, deduplicated by id + const allMetadata = Array.from( + new Map( + aiMessages + .flatMap(m => m.metadata || []) + .map(meta => [meta.id, meta]) + ).values() + ); // Use active metadata from scroll if available, otherwise use latest message's metadata - const displayMetadata = activeMetadata.length > 0 ? activeMetadata : currentMetadata; + let displayMetadata = activeMetadata.length > 0 ? activeMetadata : currentMetadata; + + // Apply philosophy/scholar filter if active + if (filterScholar) { + displayMetadata = allMetadata.filter(meta => meta.scholar === filterScholar); + } + + const handlePhilosopherClick = (scholar: string) => { + if (filterScholar === scholar) { + setFilterScholar(null); // Clear filter if clicking the already active one + } else { + setFilterScholar(scholar); + } + }; return ( <> @@ -53,7 +75,7 @@ export function Sidebar({ messages = [], activeMetadata = [], isOpen = false, on {/* Scrollable Content */}
- { /* TODO: Implement philosopher filter using scholar */ }} /> +
From 6c7566df3106613f6dc336048650e430b786e4c7 Mon Sep 17 00:00:00 2001 From: Sanghun95 Date: Tue, 3 Mar 2026 15:46:31 +0900 Subject: [PATCH 09/52] fix(pr): address CodeRabbit review feedback on backend tools and DB scripts --- backend/app/api/routes/chat.py | 2 +- backend/scripts/generate_book_mapping.py | 6 +++++- backend/scripts/generate_sql_updates.py | 4 ++-- backend/scripts/update_db_metadata.py | 2 +- backend/update_metadata.sql | 2 +- frontend/app/page.tsx | 4 ++-- frontend/components/chat/ChatMain.tsx | 2 +- frontend/components/chat/MessageList.tsx | 7 ++++++- 8 files changed, 19 insertions(+), 10 deletions(-) diff --git a/backend/app/api/routes/chat.py b/backend/app/api/routes/chat.py index 7155c88..6a5566a 100644 --- a/backend/app/api/routes/chat.py +++ b/backend/app/api/routes/chat.py @@ -23,7 +23,7 @@ class ChatRequest(BaseModel): history: List[HistoryMessage] = Field(default_factory=list) class TitleRequest(BaseModel): - query: str + query: str = Field(..., max_length=1024) def _search_documents(query_vector): return get_client().rpc( diff --git a/backend/scripts/generate_book_mapping.py b/backend/scripts/generate_book_mapping.py index 424b2e7..6f905c7 100644 --- a/backend/scripts/generate_book_mapping.py +++ b/backend/scripts/generate_book_mapping.py @@ -115,7 +115,11 @@ async def translate_book_info(file_name: str) -> dict: # If all keys exhausted or other error, fallback print(f"LLM Failed for {file_name}, falling back to Kyobo Search...") - return await kyobo_fallback(file_name, "") + name_without_ext = Path(file_name).stem + parts = name_without_ext.rsplit(" by ", 1) + fallback_title = parts[0].strip() + fallback_author = parts[1].strip() if len(parts) == 2 else "" + return await kyobo_fallback(fallback_title, fallback_author) async def search_aladin(title: str, author: str) -> dict: if not ALADIN_API_KEY: diff --git a/backend/scripts/generate_sql_updates.py b/backend/scripts/generate_sql_updates.py index 369c518..0a445ef 100644 --- a/backend/scripts/generate_sql_updates.py +++ b/backend/scripts/generate_sql_updates.py @@ -12,9 +12,9 @@ def generate_sql(): # 1. Create a B-Tree index on the title field to make string matching instant # instead of doing a full sequential table scan sql_statements = [ + "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_documents_book_title ON documents ((metadata->'book_info'->>'title'));\n\n" "BEGIN;\n", - "CREATE INDEX IF NOT EXISTS idx_documents_book_title ON documents ((metadata->'book_info'->>'title'));\n", - "SET statement_timeout = '120s'; -- Increase timeout to be safe\n" + "SET LOCAL statement_timeout = '120s'; -- Increase timeout to be safe for this transaction\n" ] for book in mapping_data: diff --git a/backend/scripts/update_db_metadata.py b/backend/scripts/update_db_metadata.py index 434a721..2a2bddd 100644 --- a/backend/scripts/update_db_metadata.py +++ b/backend/scripts/update_db_metadata.py @@ -123,7 +123,7 @@ def update_doc(doc): return True except Exception as e: if attempt < max_retries - 1: - sleep(0.5 * (attempt + 1)) # Exponential backoff + sleep(0.5 * (2 ** attempt)) # Exponential backoff continue print(f"Error updating {doc['id']}: {e}") return False diff --git a/backend/update_metadata.sql b/backend/update_metadata.sql index 0cf0877..4114ba9 100644 --- a/backend/update_metadata.sql +++ b/backend/update_metadata.sql @@ -1,4 +1,4 @@ -CREATE INDEX IF NOT EXISTS idx_documents_book_title ON documents ((metadata->'book_info'->>'title')); +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_documents_book_title ON documents ((metadata->'book_info'->>'title')); BEGIN; diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 22bb04f..394593f 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -9,7 +9,7 @@ export default function Home() { const [messages, setMessages] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false); - const [chatTitle, setChatTitle] = useState("미덕에 관한 대화"); + const [chatTitle, setChatTitle] = useState("새로운 대화"); const [activeMetadata, setActiveMetadata] = useState([]); const processLine = useCallback((line: string, eventObj: { current: string }, aiMsgId: string): boolean => { @@ -177,7 +177,7 @@ export default function Home() { isSubmitting={isSubmitting} onClearChat={() => { setMessages([]); - setChatTitle("미덕에 관한 대화"); + setChatTitle("새로운 대화"); setActiveMetadata([]); }} onMenuClick={() => setIsSidebarOpen(true)} diff --git a/frontend/components/chat/ChatMain.tsx b/frontend/components/chat/ChatMain.tsx index c9479bd..c2a7ed1 100644 --- a/frontend/components/chat/ChatMain.tsx +++ b/frontend/components/chat/ChatMain.tsx @@ -16,7 +16,7 @@ interface ChatMainProps { onVisibleMessageChange?: (meta: DocumentMetadata[]) => void; } -export function ChatMain({ messages, chatTitle = "미덕에 관한 대화", onSendMessage, isSubmitting, onClearChat, onMenuClick, onVisibleMessageChange }: ChatMainProps) { +export function ChatMain({ messages, chatTitle = "새로운 대화", onSendMessage, isSubmitting, onClearChat, onMenuClick, onVisibleMessageChange }: ChatMainProps) { const messagesEndRef = useRef(null); const [shouldAutoScroll, setShouldAutoScroll] = useState(true); const [startTime, setStartTime] = useState(""); diff --git a/frontend/components/chat/MessageList.tsx b/frontend/components/chat/MessageList.tsx index 225dc8a..b924f16 100644 --- a/frontend/components/chat/MessageList.tsx +++ b/frontend/components/chat/MessageList.tsx @@ -46,6 +46,11 @@ export function MessageList({ messages, onOpenCitation, onVisibleMessageChange } const msg = messages.find(m => m.id === mostVisibleId); if (msg && msg.metadata && msg.metadata.length > 0) { onVisibleMessageChange(msg.metadata); + } else { + const aiMessages = messages.filter(m => m.role === "ai" && m.metadata && m.metadata.length > 0); + if (aiMessages.length > 0) { + onVisibleMessageChange(aiMessages[aiMessages.length - 1].metadata!); + } } } else { const aiMessages = messages.filter(m => m.role === "ai" && m.metadata && m.metadata.length > 0); @@ -59,7 +64,7 @@ export function MessageList({ messages, onOpenCitation, onVisibleMessageChange } }); const elements = document.querySelectorAll(".ai-message-card"); - elements.forEach(el => observer.current?.observe(el)); + elements.forEach(el => { observer.current?.observe(el); }); return () => { observer.current?.disconnect(); From 9de894dc0b22872c6531c7be890d990fd97e827b Mon Sep 17 00:00:00 2001 From: Sanghun95 Date: Tue, 3 Mar 2026 16:11:05 +0900 Subject: [PATCH 10/52] fix(pr): address additional CodeRabbit comments --- backend/app/api/routes/chat.py | 4 +++- frontend/components/chat/MessageList.tsx | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/app/api/routes/chat.py b/backend/app/api/routes/chat.py index 6a5566a..bd6cafd 100644 --- a/backend/app/api/routes/chat.py +++ b/backend/app/api/routes/chat.py @@ -145,11 +145,13 @@ async def chat_title_endpoint(request: Request, title_request: TitleRequest): title = await asyncio.wait_for(generate_chat_title_async(query), timeout=10.0) # Handle case where LLM returns something too long or with quotes title = title.replace('"', '').replace("'", "").strip() + if not title: + return {"title": "새로운 대화"} if len(title) > 20: # Ensure it's short even if LLM disobeys a bit title = title[:20] + "..." return {"title": title} except asyncio.TimeoutError: - logger.warning(f"Timeout generating chat title for query: {query}") + logger.warning("Timeout generating chat title") return {"title": "새로운 대화"} except Exception: logger.exception("Failed to generate chat title") diff --git a/frontend/components/chat/MessageList.tsx b/frontend/components/chat/MessageList.tsx index b924f16..e589d8a 100644 --- a/frontend/components/chat/MessageList.tsx +++ b/frontend/components/chat/MessageList.tsx @@ -50,12 +50,16 @@ export function MessageList({ messages, onOpenCitation, onVisibleMessageChange } const aiMessages = messages.filter(m => m.role === "ai" && m.metadata && m.metadata.length > 0); if (aiMessages.length > 0) { onVisibleMessageChange(aiMessages[aiMessages.length - 1].metadata!); + } else { + onVisibleMessageChange([]); } } } else { const aiMessages = messages.filter(m => m.role === "ai" && m.metadata && m.metadata.length > 0); if (aiMessages.length > 0) { onVisibleMessageChange(aiMessages[aiMessages.length - 1].metadata!); + } else { + onVisibleMessageChange([]); } } } From 3d773d7a9480fb6773e9b395943aabaceb30aa15 Mon Sep 17 00:00:00 2001 From: Sanghun95 Date: Tue, 3 Mar 2026 16:16:19 +0900 Subject: [PATCH 11/52] style: update welcome messages and input placeholder to be more generalized --- frontend/components/chat/FloatingInput.tsx | 2 +- frontend/components/chat/MessageList.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/components/chat/FloatingInput.tsx b/frontend/components/chat/FloatingInput.tsx index de19f8f..c00ff25 100644 --- a/frontend/components/chat/FloatingInput.tsx +++ b/frontend/components/chat/FloatingInput.tsx @@ -37,7 +37,7 @@ export function FloatingInput({ onSendMessage, isSubmitting }: FloatingInputProp