ll7098ll commited on
Commit
d95e487
·
verified ·
1 Parent(s): 92a2f88

Upload 10 files

Browse files
Files changed (11) hide show
  1. .gitattributes +1 -0
  2. NanumGothic.ttf +3 -0
  3. README.md +1 -1
  4. app.py +21 -179
  5. controller.py +96 -0
  6. devcontainer.json +33 -0
  7. gitignore.txt +5 -0
  8. model.py +280 -0
  9. requirements.txt +2 -2
  10. utils.py +6 -0
  11. 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.45.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 os
2
- import time
3
- import streamlit as st
4
- from PIL import Image # Pillow is imported but not used, let's keep it for potential future image features
5
- import google.generativeai as genai
6
- from streamlit_extras.colored_header import colored_header
7
- import markdown
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
- # Streamlit 인터페이스 설정
89
- colored_header(
90
- label="계획서 보고서 작성 AI",
91
- description="PDF 서식 및 참고 파일을 업로드하고 지시사항을 입력하면 계획서 또는 보고서를 작성해줍니다.",
92
- color_name="blue-70",
 
 
 
 
 
 
 
 
 
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
+ )