Spaces:
Paused
Paused
Upload 10 files
Browse files- .gitattributes +1 -0
- NanumGothic.ttf +3 -0
- README.md +1 -1
- app.py +21 -179
- controller.py +96 -0
- devcontainer.json +33 -0
- gitignore.txt +5 -0
- model.py +280 -0
- requirements.txt +2 -2
- utils.py +6 -0
- view.py +127 -0
.gitattributes
CHANGED
|
@@ -46,3 +46,4 @@ dist/
|
|
| 46 |
.tox/
|
| 47 |
.ipynb_checkpoints
|
| 48 |
.vscode/
|
|
|
|
|
|
| 46 |
.tox/
|
| 47 |
.ipynb_checkpoints
|
| 48 |
.vscode/
|
| 49 |
+
NanumGothic.ttf filter=lfs diff=lfs merge=lfs -text
|
NanumGothic.ttf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:48a28e97b34fc8e5b157657633670cd1b7de126cfc414da65ce9c3d5bc8be733
|
| 3 |
+
size 4691820
|
README.md
CHANGED
|
@@ -4,7 +4,7 @@ emoji: 🔥
|
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: purple
|
| 6 |
sdk: streamlit
|
| 7 |
-
sdk_version: 1.
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: cc
|
|
|
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: purple
|
| 6 |
sdk: streamlit
|
| 7 |
+
sdk_version: 1.43.2
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: cc
|
app.py
CHANGED
|
@@ -1,183 +1,25 @@
|
|
| 1 |
-
import
|
| 2 |
-
import
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
import PyPDF2 # PDF 처리 라이브러리
|
| 9 |
-
|
| 10 |
-
# Google Gemini API 키 설정
|
| 11 |
-
genai.configure(api_key=os.environ["GEMINI_API_KEY"])
|
| 12 |
-
|
| 13 |
-
# 모델 설정
|
| 14 |
-
generation_config = {
|
| 15 |
-
"temperature": 0.7, # 보고서/계획서 톤에 맞춰 temperature 낮춤
|
| 16 |
-
"top_p": 0.95,
|
| 17 |
-
"top_k": 40,
|
| 18 |
-
"max_output_tokens": 10000,
|
| 19 |
-
"response_mime_type": "text/plain",
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
model = genai.GenerativeModel(
|
| 23 |
-
model_name="gemini-2.0-flash-exp", # 필요에 따라 모델 변경
|
| 24 |
-
generation_config=generation_config,
|
| 25 |
)
|
| 26 |
|
| 27 |
-
# PDF 텍스트 추출 함수 (기존 함수 재활용)
|
| 28 |
-
def extract_text_from_pdf(uploaded_pdf_file):
|
| 29 |
-
"""PDF 파일에서 텍스트를 추출합니다."""
|
| 30 |
-
text = ""
|
| 31 |
-
if uploaded_pdf_file is not None: # 파일이 업로드되었는지 확인
|
| 32 |
-
try:
|
| 33 |
-
pdf_reader = PyPDF2.PdfReader(uploaded_pdf_file)
|
| 34 |
-
for page_num in range(len(pdf_reader.pages)):
|
| 35 |
-
page = pdf_reader.pages[page_num]
|
| 36 |
-
text += page.extract_text()
|
| 37 |
-
except Exception as e:
|
| 38 |
-
st.error(f"PDF 파일 처리 오류: {e}")
|
| 39 |
-
return None
|
| 40 |
-
return text
|
| 41 |
-
|
| 42 |
-
def generate_report_or_plan(pdf_template_text, reference_pdf_text, user_instructions):
|
| 43 |
-
"""
|
| 44 |
-
PDF 템플릿, 참고 PDF, 사용자 지시사항을 기반으로 보고서 또는 계획서를 생성합니다.
|
| 45 |
-
"""
|
| 46 |
-
full_text = ""
|
| 47 |
-
try:
|
| 48 |
-
prompt_text = f"""
|
| 49 |
-
# 계획서 또는 보고서 작성
|
| 50 |
-
|
| 51 |
-
## PDF 서식 파일 내용:
|
| 52 |
-
```
|
| 53 |
-
{pdf_template_text}
|
| 54 |
-
```
|
| 55 |
-
|
| 56 |
-
"""
|
| 57 |
-
if reference_pdf_text: # 참고 파일 내용이 있을 경우에만 추가
|
| 58 |
-
prompt_text += f"""
|
| 59 |
-
## 참고 파일 PDF 내용:
|
| 60 |
-
```
|
| 61 |
-
{reference_pdf_text}
|
| 62 |
-
```
|
| 63 |
-
"""
|
| 64 |
-
|
| 65 |
-
prompt_text += f"""
|
| 66 |
-
## 사용자 지시사항:
|
| 67 |
-
{user_instructions}
|
| 68 |
-
|
| 69 |
-
---
|
| 70 |
-
|
| 71 |
-
**지시사항에 따라 PDF 템플릿{", 참고 PDF" if reference_pdf_text else ""}를 분석하고, 내용을 채워 계획서 또는 보고서를 작성하세요.**
|
| 72 |
-
**한국어로 작성하며, 명확하고 논리적인 구조로 작성해주세요.**
|
| 73 |
-
**템플릿 양식에 맞춰 내용을 작성하고, 필요하다면 추가적인 정보나 내용을 생성해도 좋습니다.**
|
| 74 |
-
**만약 템플릿 내용이 부족하거나 지시사항을 수행하기 어렵다면, 솔직하게 답변해주세요.**
|
| 75 |
-
**학교 사업 계획서, 교육 활동 계획서, 프로젝트 학습 계획서 등 교육 관련 계획서 및 보고서 작성에 특화되어 있습니다.**
|
| 76 |
-
"""
|
| 77 |
-
|
| 78 |
-
response = model.generate_content([prompt_text])
|
| 79 |
-
for chunk in response.text:
|
| 80 |
-
full_text += chunk
|
| 81 |
-
time.sleep(0.01)
|
| 82 |
-
st.session_state.generated_result += chunk
|
| 83 |
-
output_area.markdown(st.session_state.generated_result, unsafe_allow_html=True)
|
| 84 |
-
except Exception as e:
|
| 85 |
-
st.error(f"보고서/계획서 생성 중 오류 발생: {str(e)}")
|
| 86 |
-
return full_text
|
| 87 |
|
| 88 |
-
#
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
)
|
| 94 |
-
|
| 95 |
-
# 파일 업로드 버튼 (PDF 파일만 허용)
|
| 96 |
-
uploaded_pdf_template_file = st.file_uploader("PDF 서식 파일 업로드", type=["pdf"])
|
| 97 |
-
uploaded_reference_pdf_file = st.file_uploader("참고 파일 PDF 업로드 (선택 사항)", type=["pdf"]) # 참고 파일 업로드 추가
|
| 98 |
-
|
| 99 |
-
# 지시사항 입력 텍스트 영역 추가 (placeholder, height 조정)
|
| 100 |
-
user_instructions = st.text_area(
|
| 101 |
-
"작성 지시사항 입력",
|
| 102 |
-
placeholder="""예시:
|
| 103 |
-
- 이 PDF 템플릿을 사용하여 2025학년도 1-1-1 프로젝트 학습 계획 초안을 작성해주세요. 핵심 목표는 학생 중심 교육 강화입니다. 성취 기준은...
|
| 104 |
-
- [참고 파일 PDF] 제출된 교육 활동 계획서 PDF를 참고하여, 계획의 타당성을 분석하고, 개선점을 3가지 제안해주세요.
|
| 105 |
-
- 이 프로젝트 학습 계획서 템플릿을 사용하여, '기후 변화와 우리'라는 주제로 5학년 학생 대상의 프로젝트 학습 계획서를 작성해주세요. 탐구 단계를 구체적으로 작성해주세요.
|
| 106 |
-
""",
|
| 107 |
-
height=200 # 높이 조정
|
| 108 |
-
)
|
| 109 |
-
|
| 110 |
-
generate_button = st.button("보고서/계획서 생성")
|
| 111 |
-
|
| 112 |
-
output_area = st.empty()
|
| 113 |
-
|
| 114 |
-
if generate_button and uploaded_pdf_template_file and user_instructions:
|
| 115 |
-
try:
|
| 116 |
-
pdf_template_text = extract_text_from_pdf(uploaded_pdf_template_file)
|
| 117 |
-
reference_pdf_text = extract_text_from_pdf(uploaded_reference_pdf_file) # 참고 파일 텍스트 추출
|
| 118 |
-
|
| 119 |
-
if pdf_template_text:
|
| 120 |
-
st.session_state.generated_result = ""
|
| 121 |
-
with st.spinner("보고서/계획서 생성 중..."): # 로딩 스피너 추가
|
| 122 |
-
result = generate_report_or_plan(pdf_template_text, reference_pdf_text, user_instructions) # 참고 파일 텍스트 추가
|
| 123 |
-
|
| 124 |
-
html_text = markdown.markdown(result, extensions=['tables'])
|
| 125 |
-
output_area.markdown(html_text, unsafe_allow_html=True)
|
| 126 |
-
|
| 127 |
-
else:
|
| 128 |
-
st.warning("PDF 서식 파일에서 텍스트를 추출하는데 실패했습니다. 파일 형식을 확인해주세요.")
|
| 129 |
-
|
| 130 |
-
except Exception as e:
|
| 131 |
-
st.error(f"보고서/계획서 생성 중 오류 발생: {str(e)}")
|
| 132 |
-
|
| 133 |
-
elif generate_button and not uploaded_pdf_template_file:
|
| 134 |
-
st.warning("PDF 서식 파일을 업로드하세요.")
|
| 135 |
-
elif generate_button and not user_instructions:
|
| 136 |
-
st.warning("작성 지시사항을 입력하세요.")
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
# FAQ (계획서 보고서 작성에 맞게 수정)
|
| 140 |
-
with st.expander("❓ 계획서 보고서 작성 AI FAQ"):
|
| 141 |
-
st.write("""
|
| 142 |
-
**Q1. 계획서 보고서 작성 AI는 어떤 기능을 제공하나요?**
|
| 143 |
-
|
| 144 |
-
A. 이 앱은 학교에서 필요한 다양한 **계획서 및 보고서** 작성을 돕기 위해 개발된 AI 도구입니다. PDF 서식 파일, **참고 파일 PDF (선택 사항)**, 그리고 작성 지시사항을 입력하면, AI가 서식에 맞춰 계획서 또는 보고서 초안을 생성합니다. 사업 계획서, 교육 활동 계획서, 프로젝트 학습 계획서, 각종 보고서 등 다양한 문서 작성을 지원합니다. 생성된 초안은 필요에 따라 수정 및 보완하여 완성할 수 있습니다.
|
| 145 |
-
|
| 146 |
-
**Q2. 어떤 종류의 계획서 및 보고서 작성을 지원하나요?**
|
| 147 |
-
|
| 148 |
-
A. 주로 학교 현장에서 사용되는 계획서 및 보고서 작성을 지원합니다. 예시는 다음과 같습니다:
|
| 149 |
-
* **사업 계획서:** 학교 발전 계획, 특정 사업 운영 계획 등
|
| 150 |
-
* **교육 활동 계획서:** 수업 계획, 방과후학교 운영 계획, 창의적 체험활동 계획 등
|
| 151 |
-
* **프로젝트 학습 계획서:** 학생 주도 프로젝트 학습 운영 계획
|
| 152 |
-
* **각종 보고서:** 활동 결과 보고서, 사업 결과 보고서, 평가 보고서 등
|
| 153 |
-
* (향후 지원 확대 예정)
|
| 154 |
-
|
| 155 |
-
**Q3. PDF 서식 파일은 어떻게 활용하나요?**
|
| 156 |
-
|
| 157 |
-
A. **PDF 서식 파일은 계획서 또는 보고서의 템플릿 역할**을 합니다. 기존에 사용하던 서식 파일(.pdf)을 업로드하면, AI가 해당 서식에 맞춰 내용을 채워줍니다. 별도의 서식 파일 없이 백지 상태에서 내용을 생성하고 싶다면, 비어있는 PDF 파일을 업로드하거나, 지시사항에 '자유 형식으로 작성해줘' 와 같이 요청할 수 있습니다.
|
| 158 |
-
|
| 159 |
-
**Q3-1. 참고 파일 PDF는 어떻게 활용하나요?**
|
| 160 |
-
|
| 161 |
-
A. **참고 파일 PDF는 AI가 보고서/계획서를 작성할 때 참고할 추가 정보**를 제공합니다. 예를 들어, '참고 파일 PDF를 바탕으로 ~를 분석해주세요' 와 같은 지시사항과 함께 참고 PDF를 업로드하면, AI가 해당 PDF 내용을 분석하여 보고서/계획서 작성에 활용합니다. **참고 파일 PDF는 선택 사항**이며, 필수가 아닙니다.
|
| 162 |
-
|
| 163 |
-
**Q4. 작성 지시사항은 어떻게 입력해야 하나요?**
|
| 164 |
-
|
| 165 |
-
A. **구체적이고 명확하게 지시사항을 입력**할수록 AI가 더 정확하게 이해하고 원하는 결과물을 생성할 수 있습니다. 다음과 같은 내용을 포함하여 지시사항을 작성해보세요:
|
| 166 |
-
* **작성할 문서의 종류:** (예: 2025학년도 각종 사업 계획서, OOO 프로젝트 보고서, OOO 교육 활동 계획서 초안)
|
| 167 |
-
* **주요 내용 및 핵심 목표:** (예: 학생 중심 교육 강화, 창의적 체험활동 활성화, OOO 사업 성과 분석)
|
| 168 |
-
* **참고 자료:** (**참고 파일 PDF** 외에 추가적으로 참고할 내용이 있다면 간략하게 언급, **참고 파일 PDF 활용 지시 포함**)
|
| 169 |
-
* **특정 양식 요청:** (예: 표 형식으로 작성, 핵심 내용만 요약, 자유 형식으로 작성)
|
| 170 |
-
* **분량:** (예: A4 2장 내외로 요약)
|
| 171 |
-
|
| 172 |
-
**Q5. 계획서/보고서 생성 후 수정은 어떻게 하나요?**
|
| 173 |
-
|
| 174 |
-
A. 계획서/보고서 생성 후, 하단 출력 내용을 확인��고, 필요한 경우 **텍스트를 선택하여 복사**한 후, 워드프로세서(MS Word, 한글 등)에 붙여넣어 수정할 수 있습니다. 향후 챗봇 기능을 추가하여 앱 내에서 직접 수정하고 추가적인 요청을 할 수 있도록 개선할 예정입니다.
|
| 175 |
-
|
| 176 |
-
**Q6. PDF 파일 텍스트 추출 오류가 발생할 경우 어떻게 해야 하나요?**
|
| 177 |
-
|
| 178 |
-
A. PDF 파일이 이미지 형태로 스캔된 경우, 텍스트 추출이 제대로 이루어지지 않을 수 있습니다. 가능하다면 **텍스트 기반 PDF 파일**을 사용하거나, OCR (광학 문자 인식) 기능을 활용하여 텍스트를 추출한 후 다시 시도해보세요. 향후 OCR 기능 내장 또는 PDF 텍스트 추출 성능 향상을 위해 지속적으로 개선할 예정입니다.
|
| 179 |
-
|
| 180 |
-
**Q7. 지원하는 출력 형식은 무엇인가요?**
|
| 181 |
-
|
| 182 |
-
A. 현재는 **Markdown 형식**으로 결과를 출력합니다. 향후 **Word 문서(.docx) 다운로드** 기능을 추가하여 사용 편의성을 높일 계획입니다. PDF 다운로드 기능은 추후 검토하겠습니다.
|
| 183 |
-
""")
|
|
|
|
| 1 |
+
from controller import click_generate_btn
|
| 2 |
+
from view import (
|
| 3 |
+
display_header,
|
| 4 |
+
display_file_uploaders,
|
| 5 |
+
display_instructions_input,
|
| 6 |
+
display_generate_button,
|
| 7 |
+
display_faq,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
)
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
# --- UI 랜더링 ---
|
| 12 |
+
display_header()
|
| 13 |
+
uploaded_template_file, uploaded_reference_file = display_file_uploaders()
|
| 14 |
+
user_instructions = display_instructions_input()
|
| 15 |
+
generate_button_clicked = display_generate_button()
|
| 16 |
+
display_faq()
|
| 17 |
+
|
| 18 |
+
click_generate_btn(
|
| 19 |
+
(
|
| 20 |
+
uploaded_template_file,
|
| 21 |
+
uploaded_reference_file,
|
| 22 |
+
user_instructions,
|
| 23 |
+
generate_button_clicked,
|
| 24 |
+
)
|
| 25 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
controller.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import markdown
|
| 2 |
+
import time # For small delay in streaming view
|
| 3 |
+
import view
|
| 4 |
+
import model
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def click_generate_btn(props):
|
| 8 |
+
(
|
| 9 |
+
uploaded_template_file,
|
| 10 |
+
uploaded_reference_file,
|
| 11 |
+
user_instructions,
|
| 12 |
+
generate_button_clicked,
|
| 13 |
+
) = props
|
| 14 |
+
if generate_button_clicked:
|
| 15 |
+
# 새 결과 생성 시 이전 세션 값 초기화
|
| 16 |
+
view.st.session_state.pop("last_result_md", None)
|
| 17 |
+
view.st.session_state.pop("last_docx_data", None)
|
| 18 |
+
|
| 19 |
+
# 1. 입력 유효성 검사
|
| 20 |
+
if not uploaded_template_file:
|
| 21 |
+
view.display_warning("PDF 서식 파일을 업로드하세요.")
|
| 22 |
+
elif not user_instructions:
|
| 23 |
+
view.display_warning("작성 지시사항을 입력하세요.")
|
| 24 |
+
else:
|
| 25 |
+
# 2. PDF 텍스트 추출
|
| 26 |
+
template_text = model.extract_text_from_pdf(uploaded_template_file)
|
| 27 |
+
reference_text = model.extract_text_from_pdf(
|
| 28 |
+
uploaded_reference_file
|
| 29 |
+
) # Returns None if no file
|
| 30 |
+
|
| 31 |
+
if not template_text:
|
| 32 |
+
view.display_warning(
|
| 33 |
+
"PDF 서식 파일에서 텍스트를 추출하지 못했습니다. 파일 내용을 확인해주세요."
|
| 34 |
+
)
|
| 35 |
+
else:
|
| 36 |
+
# 3. 결과 영역 준비
|
| 37 |
+
results_container, results_placeholder = view.display_results_area()
|
| 38 |
+
|
| 39 |
+
# 4. 콘텐츠 생성 (스트리밍) 및 표시
|
| 40 |
+
full_response_md = ""
|
| 41 |
+
error_occurred = False
|
| 42 |
+
try:
|
| 43 |
+
with view.display_spinner("보고서/계획서 생성 중..."):
|
| 44 |
+
for chunk in model.generate_content_from_gemini(
|
| 45 |
+
template_text, reference_text, user_instructions
|
| 46 |
+
):
|
| 47 |
+
full_response_md += chunk
|
| 48 |
+
view.update_results_stream(
|
| 49 |
+
results_placeholder, full_response_md
|
| 50 |
+
)
|
| 51 |
+
time.sleep(0.01)
|
| 52 |
+
|
| 53 |
+
if "오류 발생:" in full_response_md:
|
| 54 |
+
error_occurred = True
|
| 55 |
+
|
| 56 |
+
# ✅ 결과 저장
|
| 57 |
+
view.st.session_state["last_result_md"] = full_response_md
|
| 58 |
+
|
| 59 |
+
except Exception as e:
|
| 60 |
+
error_occurred = True
|
| 61 |
+
view.display_error(f"콘텐츠 생성 중 심각한 오류 발생: {e}")
|
| 62 |
+
full_response_md = f"오류: {e}"
|
| 63 |
+
|
| 64 |
+
# 5. 최종 결과 처리 및 DOCX 생성/다운로드 버튼 표시
|
| 65 |
+
if not error_occurred and full_response_md:
|
| 66 |
+
final_html = markdown.markdown(
|
| 67 |
+
full_response_md, extensions=["tables", "fenced_code"]
|
| 68 |
+
)
|
| 69 |
+
view.display_final_result(results_placeholder, final_html)
|
| 70 |
+
|
| 71 |
+
title, docx_data = model.markdown_to_docx(full_response_md)
|
| 72 |
+
if docx_data:
|
| 73 |
+
# 결과는 유지하면서 다운로드 버튼 표시
|
| 74 |
+
view.st.session_state["last_docx_data"] = docx_data
|
| 75 |
+
view.display_download_button(title, docx_data)
|
| 76 |
+
else:
|
| 77 |
+
view.display_warning("결과를 DOCX로 변환하는 데 실패했습니다.")
|
| 78 |
+
|
| 79 |
+
# 결과 복원
|
| 80 |
+
# if (
|
| 81 |
+
# "last_result_md" in view.st.session_state
|
| 82 |
+
# and not generate_button_clicked
|
| 83 |
+
# ):
|
| 84 |
+
# prev_html = markdown.markdown(
|
| 85 |
+
# view.st.session_state["last_result_md"],
|
| 86 |
+
# extensions=["tables", "fenced_code"],
|
| 87 |
+
# )
|
| 88 |
+
# results_container, result_placeholder = view.display_results_area()
|
| 89 |
+
# view.display_final_result(result_placeholder, prev_html)
|
| 90 |
+
|
| 91 |
+
# if "last_docx_data" in view.st.session_state:
|
| 92 |
+
# view.display_download_button(
|
| 93 |
+
# results_container,
|
| 94 |
+
# view.st.session_state["last_docx_data"],
|
| 95 |
+
# key="download_button_restored",
|
| 96 |
+
# )
|
devcontainer.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Python 3",
|
| 3 |
+
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
| 4 |
+
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
|
| 5 |
+
"customizations": {
|
| 6 |
+
"codespaces": {
|
| 7 |
+
"openFiles": [
|
| 8 |
+
"README.md",
|
| 9 |
+
"app.py"
|
| 10 |
+
]
|
| 11 |
+
},
|
| 12 |
+
"vscode": {
|
| 13 |
+
"settings": {},
|
| 14 |
+
"extensions": [
|
| 15 |
+
"ms-python.python",
|
| 16 |
+
"ms-python.vscode-pylance"
|
| 17 |
+
]
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y <packages.txt; [ -f requirements.txt ] && pip3 install --user -r requirements.txt; pip3 install --user streamlit; echo '✅ Packages installed and Requirements met'",
|
| 21 |
+
"postAttachCommand": {
|
| 22 |
+
"server": "streamlit run app.py --server.enableCORS false --server.enableXsrfProtection false"
|
| 23 |
+
},
|
| 24 |
+
"portsAttributes": {
|
| 25 |
+
"8501": {
|
| 26 |
+
"label": "Application",
|
| 27 |
+
"onAutoForward": "openPreview"
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
"forwardPorts": [
|
| 31 |
+
8501
|
| 32 |
+
]
|
| 33 |
+
}
|
gitignore.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
__pycache__
|
| 3 |
+
.vscode
|
| 4 |
+
*.pdf
|
| 5 |
+
*.zip
|
model.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import google.generativeai as genai
|
| 3 |
+
import PyPDF2
|
| 4 |
+
import io
|
| 5 |
+
import platform
|
| 6 |
+
import re
|
| 7 |
+
from typing import Tuple
|
| 8 |
+
|
| 9 |
+
from docx import Document
|
| 10 |
+
from docx.shared import Pt
|
| 11 |
+
|
| 12 |
+
from utils import get_api_key
|
| 13 |
+
|
| 14 |
+
# --- Initialization ---
|
| 15 |
+
API_KEY = get_api_key()
|
| 16 |
+
|
| 17 |
+
if not API_KEY:
|
| 18 |
+
raise ValueError("GEMINI_API_KEY가 .env 파일에 설정되지 않았습니다.")
|
| 19 |
+
|
| 20 |
+
genai.configure(api_key=API_KEY)
|
| 21 |
+
|
| 22 |
+
# --- Configuration ---
|
| 23 |
+
GENERATION_CONFIG = {
|
| 24 |
+
"temperature": 0.7,
|
| 25 |
+
"top_p": 0.95,
|
| 26 |
+
"top_k": 40,
|
| 27 |
+
"max_output_tokens": 10000,
|
| 28 |
+
"response_mime_type": "text/plain",
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
# --- Model Instantiation ---
|
| 32 |
+
# Using a recommended model, adjust if needed
|
| 33 |
+
llm_model = genai.GenerativeModel(
|
| 34 |
+
model_name="gemini-1.5-flash", # or "gemini-pro" if preferred
|
| 35 |
+
generation_config=GENERATION_CONFIG,
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# --- Font Path Setup ---
|
| 40 |
+
# 운영체제별 한글 폰트 설정
|
| 41 |
+
def get_system_font():
|
| 42 |
+
system = platform.system()
|
| 43 |
+
if system == "Darwin": # macOS
|
| 44 |
+
return "AppleGothic"
|
| 45 |
+
elif system == "Linux":
|
| 46 |
+
return "NanumGothic"
|
| 47 |
+
elif system == "Windows":
|
| 48 |
+
return "Malgun Gothic"
|
| 49 |
+
else:
|
| 50 |
+
return "Arial" # 기본 폰트
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
SYSTEM_FONT = get_system_font()
|
| 54 |
+
|
| 55 |
+
# model.py 파일이 위치한 디렉토리를 기준으로 fonts 폴더 경로 설정
|
| 56 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 57 |
+
FONT_ROOT = os.path.join(BASE_DIR, "fonts")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# --- Core Logic Functions ---
|
| 61 |
+
def extract_text_from_pdf(uploaded_pdf_file):
|
| 62 |
+
"""PDF 파일 객체에서 텍스트를 추출합니다."""
|
| 63 |
+
text = ""
|
| 64 |
+
if uploaded_pdf_file is None:
|
| 65 |
+
return None
|
| 66 |
+
try:
|
| 67 |
+
# Reset buffer position for reading
|
| 68 |
+
uploaded_pdf_file.seek(0)
|
| 69 |
+
pdf_reader = PyPDF2.PdfReader(uploaded_pdf_file)
|
| 70 |
+
for page in pdf_reader.pages:
|
| 71 |
+
try:
|
| 72 |
+
page_text = page.extract_text()
|
| 73 |
+
if page_text:
|
| 74 |
+
text += page_text
|
| 75 |
+
except Exception:
|
| 76 |
+
# Log or handle page-specific extraction errors if needed
|
| 77 |
+
continue # Continue to next page even if one fails
|
| 78 |
+
return text if text else None # Return None if no text extracted
|
| 79 |
+
except Exception as e:
|
| 80 |
+
print(f"PDF 텍스트 추출 오류: {e}") # Log error
|
| 81 |
+
return None # Return None on error
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def generate_content_from_gemini(template_text, reference_text, instructions):
|
| 85 |
+
"""Gemini 모델을 사용하여 콘텐츠 생성을 스트리밍 방식으로 처리합니다."""
|
| 86 |
+
prompt_text = f"""
|
| 87 |
+
# 계획서 또는 보고서 작성
|
| 88 |
+
|
| 89 |
+
## PDF 서식 파일 내용:
|
| 90 |
+
{template_text}
|
| 91 |
+
|
| 92 |
+
"""
|
| 93 |
+
if reference_text:
|
| 94 |
+
prompt_text += f"""
|
| 95 |
+
## 참고 파일 PDF 내용:
|
| 96 |
+
{reference_text}
|
| 97 |
+
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
prompt_text += f"""
|
| 101 |
+
## 사용자 지시사항:
|
| 102 |
+
{instructions}
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
**지시사항에 따라 PDF 템플릿{", 참고 PDF" if reference_text else ""}를 분석하고, 내용을 채워 계획서 또는 보고서를 작성하세요.**
|
| 107 |
+
**한국어로 작성하며, 명확하고 논리적인 구조로 작성해주세요.**
|
| 108 |
+
**템플릿 양식에 맞춰 내용을 작성하고, 필요하다면 추가적인 정보나 내용을 생성해도 좋습니다.**
|
| 109 |
+
**만약 템플릿 내용이 부족하거나 지시사항을 수행하기 어렵다면, 솔직하게 답변해주세요.**
|
| 110 |
+
**학교 사업 계획서, 교육 활동 계획서, 프로젝트 학습 계획서 등 교육 관련 계획서 및 보고서 작성에 특화되어 있습니다.**
|
| 111 |
+
**표(테이블)가 필요한 경우 마크다운 테이블 형식으로 생성해주세요.**
|
| 112 |
+
"""
|
| 113 |
+
try:
|
| 114 |
+
# stream=True로 설정하여 응답을 청크 단위로 받음
|
| 115 |
+
response_stream = llm_model.generate_content([prompt_text], stream=True)
|
| 116 |
+
for chunk in response_stream:
|
| 117 |
+
# Check if the chunk has text content and it's not empty
|
| 118 |
+
if hasattr(chunk, "text") and chunk.text:
|
| 119 |
+
yield chunk.text # 각 텍스트 청크를 반환 (yield)
|
| 120 |
+
except Exception as e:
|
| 121 |
+
print(f"Gemini API 호출 오류: {e}") # Log error
|
| 122 |
+
yield f"\n\n오류 발생: 콘텐츠 생성 중 문제가 발생했습니다. ({e})" # Yield error message
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def markdown_to_docx(markdown_text: str) -> Tuple[str, io.BytesIO]:
|
| 126 |
+
"""마크다운 텍스트를 docx 문서로 변환하여 BytesIO로 반환합니다."""
|
| 127 |
+
doc = Document()
|
| 128 |
+
|
| 129 |
+
# 기본 스타일 설정
|
| 130 |
+
style = doc.styles["Normal"]
|
| 131 |
+
font = style.font
|
| 132 |
+
font.name = "Malgun Gothic" # 시스템에 맞게 조정 가능
|
| 133 |
+
font.size = Pt(11)
|
| 134 |
+
|
| 135 |
+
lines = markdown_text.strip().splitlines()
|
| 136 |
+
table_mode = False
|
| 137 |
+
table_rows = []
|
| 138 |
+
tables = []
|
| 139 |
+
title = ""
|
| 140 |
+
|
| 141 |
+
def parse_inline_styles(paragraph, text):
|
| 142 |
+
"""텍스트 내 인라인 스타일 처리: **bold**, *italic*, ***both***"""
|
| 143 |
+
pattern = r"(\*\*\*.*?\*\*\*|\*\*.*?\*\*|\*.*?\*)"
|
| 144 |
+
parts = re.split(pattern, text)
|
| 145 |
+
for part in parts:
|
| 146 |
+
run = paragraph.add_run(re.sub(r"[*]", "", part)) # 기본 텍스트
|
| 147 |
+
if part.startswith("***") and part.endswith("***"):
|
| 148 |
+
run.bold = True
|
| 149 |
+
run.italic = True
|
| 150 |
+
elif part.startswith("**") and part.endswith("**"):
|
| 151 |
+
run.bold = True
|
| 152 |
+
elif part.startswith("*") and part.endswith("*"):
|
| 153 |
+
run.italic = True
|
| 154 |
+
|
| 155 |
+
i = 0
|
| 156 |
+
while i < len(lines):
|
| 157 |
+
line = lines[i].strip()
|
| 158 |
+
if not line:
|
| 159 |
+
i += 1
|
| 160 |
+
continue
|
| 161 |
+
|
| 162 |
+
# 제목
|
| 163 |
+
if line.startswith("#"):
|
| 164 |
+
level = min(line.count("#"), 4)
|
| 165 |
+
text = line.lstrip("#").strip()
|
| 166 |
+
doc.add_heading(text, level=level)
|
| 167 |
+
table_mode = False
|
| 168 |
+
|
| 169 |
+
# 리스트
|
| 170 |
+
elif line.startswith(("- ", "* ")):
|
| 171 |
+
doc.add_paragraph(line[2:].strip(), style="List Bullet")
|
| 172 |
+
table_mode = False
|
| 173 |
+
|
| 174 |
+
elif re.match(r"^\d+\.\s", line):
|
| 175 |
+
doc.add_paragraph(re.sub(r"^\d+\.\s", "", line), style="List Number")
|
| 176 |
+
table_mode = False
|
| 177 |
+
|
| 178 |
+
# 테이블 감지
|
| 179 |
+
elif "|" in line:
|
| 180 |
+
row = [cell.strip() for cell in line.split("|") if cell.strip()]
|
| 181 |
+
table_rows.append(row)
|
| 182 |
+
|
| 183 |
+
# 다음 줄이 헤더 구분줄(---)인지 확인
|
| 184 |
+
if i + 1 < len(lines):
|
| 185 |
+
next_line = lines[i + 1].strip()
|
| 186 |
+
if re.match(r"^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$", next_line):
|
| 187 |
+
i += 1 # 구분선은 건너뛰기
|
| 188 |
+
table_rows.append("__HEADER__") # 마커로 표시
|
| 189 |
+
|
| 190 |
+
else:
|
| 191 |
+
# 이전 테이블 처리
|
| 192 |
+
if table_rows:
|
| 193 |
+
header_style = []
|
| 194 |
+
if "__HEADER__" in table_rows:
|
| 195 |
+
header_index = table_rows.index("__HEADER__")
|
| 196 |
+
headers = table_rows[header_index - 1]
|
| 197 |
+
data_rows = table_rows[header_index + 1 :]
|
| 198 |
+
all_rows = [headers] + data_rows
|
| 199 |
+
header_style = [True] + [False] * len(data_rows)
|
| 200 |
+
else:
|
| 201 |
+
all_rows = table_rows
|
| 202 |
+
header_style = [False] * len(all_rows)
|
| 203 |
+
|
| 204 |
+
num_rows = len(all_rows)
|
| 205 |
+
num_cols = max(len(row) for row in all_rows)
|
| 206 |
+
table = doc.add_table(rows=num_rows, cols=num_cols)
|
| 207 |
+
table.style = "Table Grid"
|
| 208 |
+
|
| 209 |
+
for r, row in enumerate(all_rows):
|
| 210 |
+
for c, cell in enumerate(row):
|
| 211 |
+
cell_text = cell
|
| 212 |
+
cell_obj = table.cell(r, c)
|
| 213 |
+
cell_obj.text = cell_text
|
| 214 |
+
for run in cell_obj.paragraphs[0].runs:
|
| 215 |
+
run.font.name = "Malgun Gothic"
|
| 216 |
+
if header_style[r]:
|
| 217 |
+
run.bold = True
|
| 218 |
+
table_rows = []
|
| 219 |
+
|
| 220 |
+
# 일반 문단
|
| 221 |
+
p = doc.add_paragraph()
|
| 222 |
+
parse_inline_styles(p, line)
|
| 223 |
+
|
| 224 |
+
if not title:
|
| 225 |
+
title = get_title(line)
|
| 226 |
+
i += 1
|
| 227 |
+
|
| 228 |
+
# 혹시 마지막 줄이 테이블이면 처리
|
| 229 |
+
if table_rows:
|
| 230 |
+
all_rows = table_rows
|
| 231 |
+
num_rows = len(all_rows)
|
| 232 |
+
num_cols = max(len(row) for row in all_rows)
|
| 233 |
+
table = doc.add_table(rows=num_rows, cols=num_cols)
|
| 234 |
+
table.style = "Table Grid"
|
| 235 |
+
for r, row in enumerate(all_rows):
|
| 236 |
+
for c, cell in enumerate(row):
|
| 237 |
+
cell_obj = table.cell(r, c)
|
| 238 |
+
cell_obj.text = cell
|
| 239 |
+
for run in cell_obj.paragraphs[0].runs:
|
| 240 |
+
run.font.name = "Malgun Gothic"
|
| 241 |
+
|
| 242 |
+
# 결과 저장
|
| 243 |
+
buffer = io.BytesIO()
|
| 244 |
+
doc.save(buffer)
|
| 245 |
+
buffer.seek(0)
|
| 246 |
+
return (title, buffer)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def get_title(line):
|
| 250 |
+
match = re.match(r"^#+\s*(.*)", line) # Heading tag
|
| 251 |
+
if match:
|
| 252 |
+
return match.group(1).strip()
|
| 253 |
+
|
| 254 |
+
match = re.match(r"^-+\s*(.*)", line) # Unordered list tag
|
| 255 |
+
if match:
|
| 256 |
+
return match.group(1).strip()
|
| 257 |
+
|
| 258 |
+
match = re.match(r"^\d+\.\s*(.*)", line) # Ordered list tag
|
| 259 |
+
if match:
|
| 260 |
+
return match.group(1).strip()
|
| 261 |
+
|
| 262 |
+
match = re.match(r"^>\s*(.*)", line) # Blockquote tag
|
| 263 |
+
if match:
|
| 264 |
+
return match.group(1).strip()
|
| 265 |
+
|
| 266 |
+
match = re.match(r"^`{3}.*", line) # Code block start
|
| 267 |
+
if match:
|
| 268 |
+
return "" # Code block 시작 줄은 텍스트로 간주하지 않음
|
| 269 |
+
|
| 270 |
+
match = re.match(r"^\|.*\|$", line) # Table row
|
| 271 |
+
if match:
|
| 272 |
+
# 테이블 행 내부의 텍스트를 추출 (간단하게 처리)
|
| 273 |
+
cells = [cell.strip() for cell in line.strip("|").split("|")]
|
| 274 |
+
return " ".join(cells)
|
| 275 |
+
|
| 276 |
+
match = re.match(r"^[*_]{1,3}(.*?)[*_]{1,3}$", line) # Bold, italic
|
| 277 |
+
if match:
|
| 278 |
+
return match.group(1).strip()
|
| 279 |
+
|
| 280 |
+
return line.strip()
|
requirements.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
streamlit
|
| 2 |
-
Pillow
|
| 3 |
google-generativeai
|
| 4 |
streamlit-extras
|
| 5 |
markdown
|
| 6 |
-
PyPDF2
|
|
|
|
|
|
| 1 |
streamlit
|
|
|
|
| 2 |
google-generativeai
|
| 3 |
streamlit-extras
|
| 4 |
markdown
|
| 5 |
+
PyPDF2
|
| 6 |
+
python-docx
|
utils.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def get_api_key():
|
| 5 |
+
"""환경 변수에서 API 키를 가져옵니다."""
|
| 6 |
+
return os.environ.get("GEMINI_API_KEY")
|
view.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from streamlit_extras.colored_header import colored_header
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def display_header():
|
| 6 |
+
"""페이지 상단의 헤더를 표시합니다."""
|
| 7 |
+
colored_header(
|
| 8 |
+
label="계획서 보고서 작성 AI", # Title updated slightly
|
| 9 |
+
description="PDF 서식 및 참고 파일을 업로드하고 지시사항을 입력하면 계획서 또는 보고서를 작성해줍니다.",
|
| 10 |
+
color_name="blue-70",
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def display_file_uploaders():
|
| 15 |
+
"""PDF 파일 업로더 위젯들을 표시하고 업로드된 파일 객체들을 반환합니다."""
|
| 16 |
+
uploaded_template = st.file_uploader(
|
| 17 |
+
"PDF 서식 파일 업로드", type=["pdf"], key="template_uploader"
|
| 18 |
+
)
|
| 19 |
+
uploaded_reference = st.file_uploader(
|
| 20 |
+
"참고 파일 PDF 업로드 (선택 사항)", type=["pdf"], key="reference_uploader"
|
| 21 |
+
)
|
| 22 |
+
return uploaded_template, uploaded_reference
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def display_instructions_input():
|
| 26 |
+
"""사용자 지시사항 입력 영역 위젯을 표시하고 입력된 텍스트를 반환합니다."""
|
| 27 |
+
instructions = st.text_area(
|
| 28 |
+
"작성 지시사항 입력",
|
| 29 |
+
placeholder="""예시:
|
| 30 |
+
- 이 PDF 템플릿을 사용하여 2025학년도 1-1-1 프로젝트 학습 계획 초안을 작성해주세요. 핵심 목표는 학생 중심 교육 강화입니다. 성취 기준은...
|
| 31 |
+
- [참고 파일 PDF] 제출된 교육 활동 계획서 PDF를 참고하여, 계획의 타당성을 분석하고, 개선점을 3가지 제안해주세요.
|
| 32 |
+
- 이 프로젝트 학습 계획서 템플릿을 사용하여, '기후 변화와 우리'라는 주제로 5학년 학생 대상의 프로젝트 학습 계획서를 작성해주세요. 탐구 단계를 구체적으로 작성해주세요.
|
| 33 |
+
""",
|
| 34 |
+
height=200,
|
| 35 |
+
key="instructions_input",
|
| 36 |
+
)
|
| 37 |
+
return instructions
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def display_generate_button():
|
| 41 |
+
"""'보고서/계획서 생성' 버튼 위젯을 표시하고 클릭 여부를 반환합니다."""
|
| 42 |
+
return st.button("보고서/계획서 생성", key="generate_button")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def display_results_area():
|
| 46 |
+
"""결과 표시를 위한 컨테이너와 빈 영역(placeholder)을 생성하고 반환합니다."""
|
| 47 |
+
container = st.container()
|
| 48 |
+
with container:
|
| 49 |
+
results_placeholder = st.empty()
|
| 50 |
+
return container, results_placeholder # Return both container and placeholder
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def update_results_stream(placeholder, current_text):
|
| 54 |
+
"""스트리밍 중인 텍스트를 결과 영역에 업데이트합니다."""
|
| 55 |
+
placeholder.markdown(
|
| 56 |
+
current_text + "▌", unsafe_allow_html=True
|
| 57 |
+
) # Add cursor effect
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def display_final_result(placeholder, html_content):
|
| 61 |
+
"""최종 결과를 HTML 형식으로 결과 영역에 표시합니다."""
|
| 62 |
+
placeholder.markdown(html_content, unsafe_allow_html=True)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def display_download_button(title, docx_data):
|
| 66 |
+
"""DOCX 다운로드 버튼을 표시합니다. 기존 컨테이너의 내용을 유지합니다."""
|
| 67 |
+
# 새 컨테이너를 생성하여 다운로드 버튼만 표시
|
| 68 |
+
download_container = st.container()
|
| 69 |
+
with download_container:
|
| 70 |
+
st.download_button(
|
| 71 |
+
label="📄 DOCX로 다운로드",
|
| 72 |
+
data=docx_data,
|
| 73 |
+
file_name=f"{title}.docx",
|
| 74 |
+
# mime="application/docx",
|
| 75 |
+
mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", # 정확한 MIME 타입
|
| 76 |
+
key="download_button",
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def display_error(message):
|
| 81 |
+
"""에러 메시지를 표시합니다."""
|
| 82 |
+
st.error(message)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def display_warning(message):
|
| 86 |
+
"""경고 메시지를 표시합니다."""
|
| 87 |
+
st.warning(message)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def display_spinner(message="처리 중..."):
|
| 91 |
+
"""스피너(로딩 표시) 컨텍스트를 반환합니다."""
|
| 92 |
+
return st.spinner(message)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def display_faq():
|
| 96 |
+
with st.expander("❓ 계획서 보고서 작성 AI FAQ"):
|
| 97 |
+
st.write(
|
| 98 |
+
"""
|
| 99 |
+
**Q1. 계획서 보고서 작성 AI는 어떤 기능을 제공하나요?**
|
| 100 |
+
A. 이 앱은 학교에서 필요한 다양한 **계획서 및 보고서** 작성을 돕기 위해 개발된 AI 도구입니다. PDF 서식 파일, **참고 파일 PDF (선택 사항)**, 그리고 작성 지시사항을 입력하면, AI가 서식에 맞춰 계획서 또는 보고서 초안을 생성합니다. 사업 계획서, 교육 활동 계획서, 프로젝트 학습 계획서, 각종 보고서 등 다양한 문서 작성을 지원합니다. 생성된 초안은 필요에 따라 수정 및 보완하여 완성할 수 있습니다.
|
| 101 |
+
**Q2. 어떤 종류의 계획서 및 보고서 작성을 지원하나요?**
|
| 102 |
+
A. 주로 학교 현장에서 사용되는 계획서 및 보고서 작성을 지원합니다. 예시는 다음과 같습니다:
|
| 103 |
+
* **사업 계획서:** 학교 발전 계획, 특정 사업 운영 계획 등
|
| 104 |
+
* **교육 활동 계획서:** 수업 계획, 방과후학교 운영 계획, 창의적 체험활동 계획 등
|
| 105 |
+
* **프로젝트 학�� 계획서:** 학생 주도 프로젝트 학습 운영 계획
|
| 106 |
+
* **각종 보고서:** 활동 결과 보고서, 사업 결과 보고서, 평가 보고서 등
|
| 107 |
+
* (향후 지원 확대 예정)
|
| 108 |
+
**Q3. PDF 서식 파일은 어떻게 활용하나요?**
|
| 109 |
+
A. **PDF 서식 파일은 계획서 또는 보고서의 템플릿 역할**을 합니다. 기존에 사용하던 서식 파일(.pdf)을 업로드하면, AI가 해당 서식에 맞춰 내용을 채워줍니다. 별도의 서식 파일 없이 백지 상태에서 내용을 생성하고 싶다면, 비어있는 PDF 파일을 업로드하거나, 지시사항에 '자유 형식으로 작성해줘' 와 같이 요청할 수 있습니다.
|
| 110 |
+
**Q3-1. 참고 파일 PDF는 어떻게 활용하나요?**
|
| 111 |
+
A. **참고 파일 PDF는 AI가 보고서/계획서를 작성할 때 참고할 추가 정보**를 제공합니다. 예를 들어, '참고 파일 PDF를 바탕으로 ~를 분석해주세요' 와 같은 지시사항과 함께 참고 PDF를 업로드하면, AI가 해당 PDF 내용을 분석하여 보고서/계획서 작성에 활용합니다. **참고 파일 PDF는 선택 사항**이며, 필수가 아닙니다.
|
| 112 |
+
**Q4. 작성 지시사항은 어떻게 입력해야 하나요?**
|
| 113 |
+
A. **구체적이고 명확하게 지시사항을 입력**할수록 AI가 더 정확하게 이해하고 원하는 결과물을 생성할 수 있습니다. 다음과 같은 내용을 포함하여 지시사항을 작성해보세요:
|
| 114 |
+
* **작성할 문서의 종류:** (예: 2025학년도 각종 사업 계획서, OOO 프로젝트 보고서, OOO 교육 활동 계획서 초안)
|
| 115 |
+
* **주요 내용 및 핵심 목표:** (예: 학생 중심 교육 강화, 창의적 체험활동 활성화, OOO 사업 성과 분석)
|
| 116 |
+
* **참고 자료:** (**참고 파일 PDF** 외에 추가적으로 참고할 내용이 있다면 간략하게 언급, **참고 파일 PDF 활용 지시 포함**)
|
| 117 |
+
* **특정 양식 요청:** (예: 표 형식으로 작성, 핵심 내용만 요약, 자유 형식으로 작성)
|
| 118 |
+
* **분량:** (예: A4 2장 내외로 요약)
|
| 119 |
+
**Q5. 계획서/보고서 생성 후 수정은 어떻게 하나요?**
|
| 120 |
+
A. 계획서/보고서 생성 후, 하단 출력 내용을 확인하고, 필요한 경우 **텍스트를 선택하여 복사**한 후, 워드프로세서(MS Word, 한글 등)에 붙여넣어 수정할 수 있습니다. 향후 챗봇 기능을 추가하여 앱 내에서 직접 수정하고 추가적인 요청을 할 수 있도록 개선할 예정입니다.
|
| 121 |
+
**Q6. PDF 파일 텍스트 추출 오류가 발생할 경우 어떻게 해야 하나요?**
|
| 122 |
+
A. PDF 파일이 이미지 형태로 스캔된 경우, 텍스트 추출이 제대로 이루어지지 않을 수 있습니다. 가능하다면 **텍스트 기반 PDF 파일**을 사용하거나, OCR (광학 문자 인식) 기능을 활용하여 텍스트를 추출한 후 다시 시도해보세요. 향후 OCR 기능 내장 또는 PDF 텍스트 추출 성능 향상을 위해 지속적으로 개선할 예정입니다.
|
| 123 |
+
**Q7. 지원하는 출력 형식은 무엇인가요?**
|
| 124 |
+
A. 현재는 **Markdown 형식**으로 결과를 출력합니다.
|
| 125 |
+
* **DOCX 다운로드**버튼으로 Word 문서(.docx)로 다운받을 수 있습니다.
|
| 126 |
+
"""
|
| 127 |
+
)
|