github-actions commited on
Commit
1f9f4e8
·
1 Parent(s): 9381502

Sync from GitHub acfe9adb279da37ccbdfab2b81a069691b8efd2a

Browse files
.streamlit/config.toml CHANGED
@@ -3,3 +3,4 @@ gatherUsageStats = false
3
 
4
  [server]
5
  headless = true
 
 
3
 
4
  [server]
5
  headless = true
6
+ enableStaticServing = true
README.md CHANGED
@@ -54,7 +54,8 @@ pinned: false
54
  6. [의사결정 기록](docs/06_decision_log.md)
55
  7. [트러블슈팅](docs/07_troubleshooting.md)
56
  8. [GitHub / Hugging Face 배포 자동화](docs/08_publish_automation.md)
57
- 9. [개발](docs/devlog/2026-06-07.md)
 
58
 
59
  ## 실행 방법
60
 
 
54
  6. [의사결정 기록](docs/06_decision_log.md)
55
  7. [트러블슈팅](docs/07_troubleshooting.md)
56
  8. [GitHub / Hugging Face 배포 자동화](docs/08_publish_automation.md)
57
+ 9. [모바 앱처럼 사용하기](docs/09_mobile_pwa.md)
58
+ 10. [개발일지](docs/devlog/2026-06-07.md)
59
 
60
  ## 실행 방법
61
 
docs/09_mobile_pwa.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 09. 모바일 앱처럼 사용하기
2
+
3
+ 이 프로젝트는 Chrome 모바일에서 홈 화면에 추가해 앱처럼 열 수 있도록 PWA 설정을 포함합니다.
4
+
5
+ ## 적용된 설정
6
+
7
+ - `static/manifest.webmanifest`: 앱 이름, 아이콘, 테마 색상, 표시 방식 설정
8
+ - `static/service-worker.js`: 정적 리소스 캐시용 service worker
9
+ - `static/icons/`: 홈 화면 아이콘
10
+ - `.streamlit/config.toml`: Streamlit 정적 파일 서빙 활성화
11
+ - `streamlit_app.py`: manifest, 아이콘, 모바일 메타태그 주입
12
+
13
+ ## Android Chrome에서 설치하는 방법
14
+
15
+ 1. Chrome에서 배포된 앱 주소로 접속합니다.
16
+ 2. 오른쪽 위 메뉴를 누릅니다.
17
+ 3. `홈 화면에 추가` 또는 `앱 설치`를 선택합니다.
18
+ 4. 홈 화면에 `Cert Study` 아이콘이 생기면 실행합니다.
19
+
20
+ ## 주의사항
21
+
22
+ - PWA 설치는 보통 HTTPS 환경에서 가장 안정적으로 동작합니다.
23
+ - 로컬 `http://localhost`에서는 테스트가 가능하지만, 실제 모바일 설치 확인은 배포 주소에서 하는 것이 좋습니다.
24
+ - Streamlit 앱 특성상 완전한 오프라인 문제풀이 앱은 아닙니다.
25
+ - 현재 service worker는 앱 전체 데이터를 오프라인 저장하지 않고, manifest와 아이콘 같은 정적 리소스만 캐시합니다.
26
+
27
+ ## 확인할 것
28
+
29
+ Chrome 개발자도구의 Application 탭에서 아래 항목을 확인할 수 있습니다.
30
+
31
+ - Manifest
32
+ - Service Workers
33
+ - Cache Storage
34
+
35
+ Hugging Face Space나 Oracle Cloud 배포 후 모바일 Chrome에서 접속하면 홈 화면 추가를 테스트할 수 있습니다.
static/icons/icon-maskable.svg ADDED
static/icons/icon.svg ADDED
static/manifest.webmanifest ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Cert Study App",
3
+ "short_name": "Cert Study",
4
+ "description": "자격증 문제 PDF를 구조화하고 모바일에서 문제를 풀 수 있는 학습 앱입니다.",
5
+ "lang": "ko-KR",
6
+ "start_url": "/",
7
+ "scope": "/",
8
+ "display": "standalone",
9
+ "orientation": "portrait-primary",
10
+ "background_color": "#f8fafc",
11
+ "theme_color": "#2563eb",
12
+ "categories": ["education", "productivity"],
13
+ "icons": [
14
+ {
15
+ "src": "/app/static/icons/icon.svg",
16
+ "sizes": "any",
17
+ "type": "image/svg+xml",
18
+ "purpose": "any"
19
+ },
20
+ {
21
+ "src": "/app/static/icons/icon-maskable.svg",
22
+ "sizes": "any",
23
+ "type": "image/svg+xml",
24
+ "purpose": "maskable"
25
+ }
26
+ ]
27
+ }
static/service-worker.js ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const CACHE_NAME = "cert-study-static-v1";
2
+ const STATIC_ASSETS = [
3
+ "/app/static/manifest.webmanifest",
4
+ "/app/static/icons/icon.svg",
5
+ "/app/static/icons/icon-maskable.svg"
6
+ ];
7
+
8
+ self.addEventListener("install", (event) => {
9
+ event.waitUntil(
10
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))
11
+ );
12
+ self.skipWaiting();
13
+ });
14
+
15
+ self.addEventListener("activate", (event) => {
16
+ event.waitUntil(
17
+ caches
18
+ .keys()
19
+ .then((keys) =>
20
+ Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))
21
+ )
22
+ );
23
+ self.clients.claim();
24
+ });
25
+
26
+ self.addEventListener("fetch", (event) => {
27
+ const request = event.request;
28
+ if (request.method !== "GET" || !request.url.includes("/app/static/")) {
29
+ return;
30
+ }
31
+
32
+ event.respondWith(
33
+ caches.match(request).then((cached) => {
34
+ if (cached) {
35
+ return cached;
36
+ }
37
+ return fetch(request).then((response) => {
38
+ const copy = response.clone();
39
+ caches.open(CACHE_NAME).then((cache) => cache.put(request, copy));
40
+ return response;
41
+ });
42
+ })
43
+ );
44
+ });
streamlit_app.py CHANGED
@@ -8,6 +8,7 @@ from dotenv import load_dotenv
8
  load_dotenv()
9
 
10
  import streamlit as st
 
11
  from sqlalchemy import func
12
 
13
  from cert_study_app.config import DEFAULT_USER, ensure_runtime_dirs
@@ -40,6 +41,55 @@ EMBEDDING_MODEL_OPTIONS = [
40
  ]
41
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  def get_service():
44
  db = SessionLocal()
45
  return db, QuizService(db)
@@ -1889,6 +1939,7 @@ def main():
1889
  ensure_runtime_dirs()
1890
  init_db(verbose=False)
1891
  init_state()
 
1892
  apply_mobile_styles()
1893
 
1894
  st.title("Cert Study")
 
8
  load_dotenv()
9
 
10
  import streamlit as st
11
+ import streamlit.components.v1 as components
12
  from sqlalchemy import func
13
 
14
  from cert_study_app.config import DEFAULT_USER, ensure_runtime_dirs
 
41
  ]
42
 
43
 
44
+ def inject_pwa_assets():
45
+ components.html(
46
+ """
47
+ <script>
48
+ (function () {
49
+ const doc = window.parent.document;
50
+
51
+ function ensureLink(rel, href, attrs) {
52
+ if (doc.querySelector(`link[rel="${rel}"][href="${href}"]`)) {
53
+ return;
54
+ }
55
+ const link = doc.createElement("link");
56
+ link.rel = rel;
57
+ link.href = href;
58
+ Object.entries(attrs || {}).forEach(([key, value]) => link.setAttribute(key, value));
59
+ doc.head.appendChild(link);
60
+ }
61
+
62
+ function ensureMeta(name, content) {
63
+ let meta = doc.querySelector(`meta[name="${name}"]`);
64
+ if (!meta) {
65
+ meta = doc.createElement("meta");
66
+ meta.name = name;
67
+ doc.head.appendChild(meta);
68
+ }
69
+ meta.content = content;
70
+ }
71
+
72
+ ensureLink("manifest", "/app/static/manifest.webmanifest");
73
+ ensureLink("icon", "/app/static/icons/icon.svg", { type: "image/svg+xml" });
74
+ ensureLink("apple-touch-icon", "/app/static/icons/icon.svg");
75
+ ensureMeta("theme-color", "#2563eb");
76
+ ensureMeta("apple-mobile-web-app-capable", "yes");
77
+ ensureMeta("apple-mobile-web-app-title", "Cert Study");
78
+ ensureMeta("mobile-web-app-capable", "yes");
79
+
80
+ if ("serviceWorker" in window.parent.navigator) {
81
+ window.parent.navigator.serviceWorker
82
+ .register("/app/static/service-worker.js")
83
+ .catch(function () {});
84
+ }
85
+ })();
86
+ </script>
87
+ """,
88
+ height=0,
89
+ width=0,
90
+ )
91
+
92
+
93
  def get_service():
94
  db = SessionLocal()
95
  return db, QuizService(db)
 
1939
  ensure_runtime_dirs()
1940
  init_db(verbose=False)
1941
  init_state()
1942
+ inject_pwa_assets()
1943
  apply_mobile_styles()
1944
 
1945
  st.title("Cert Study")