diff --git a/README.md b/README.md
index 1b64902..c9adb72 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,11 @@
* **고속 워크플로우 (Instant Edit)**: 메모 카드 위에 마우스를 올리고 `e`를 누르면 즉시 수정 모드 진입. 모달 클릭의 피로감을 제로로 만듭니다.
* **드래그 앤 드롭 링크**: 메모 카드를 작성기(Composer)로 드래그하여 즉시 참조 링크를 삽입하세요.
+### 🛠️ 패치 노트 (2026-04-19)
+* **파일 첨부 접근성 개선**: 지식 작성기(Composer)에 명시적인 파일 첨부(📎) 버튼을 추가했습니다. (드래그 앤 드롭과 병행 가능)
+* **모바일 UX 최적화**: 모바일 기기에서도 조작이 편리하도록 "파일추가" 텍스트 레이블을 추가했습니다.
+* **다국어 지원 안정화**: 첨부파일 관련 툴팁 및 레이블에 한/영 i18n을 적용했습니다.
+
---
## 🆚 memos vs 뇌사료 (Comparison)
@@ -127,6 +132,11 @@ We provide a security model where user data is practically undecipherable. Built
- **Instant Edit (e-key)**: Hover over a memo and press `e` to jump straight into editing mode. Zero-click productivity.
- **Drag & Drop Workflow**: Drag memo cards into the composer to instantly insert a semantic reference.
+### 🛠️ Patch Notes (2026-04-19)
+- **Improved Attachment Accessibility**: Added a dedicated Attach File (📎) button to the Composer.
+- **Mobile UI Optimization**: Added an "Add File" text label next to the icon on mobile devices for better touch usability.
+- **I18n Stabilization**: Implemented full Korean/English translation for all attachment-related UI elements.
+
---
## 🆚 memos vs Brain Dogfood (Comparison)
diff --git a/docs/Bug/20260418_missing_brain_py.md b/docs/Bug/20260418_missing_brain_py.md
deleted file mode 100644
index 6024304..0000000
--- a/docs/Bug/20260418_missing_brain_py.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# 버그 리포트: Stable 레포지토리 brain.py 누락
-
-## 버그 내용
-- `brain_dogfood_stable` 레포지토리에 Flask 애플리케이션의 진입점인 `brain.py` 파일이 누락되어 서버 구동이 불가능했던 현상.
-
-## 조치 사항
-1. 루트 디렉토리의 `brain.py` 파일을 `brain_dogfood_stable` 디렉토리로 복사.
-2. Git commit 및 원격 저장소(`origin main`)에 푸시 완료.
-3. `tools/sync_stable.py`와 `tools/final_audit.py`에서 `brain.py`를 제외 목록에서 삭제하여 향후 자동 동기화되도록 조치.
-
-## 향후 주의사항
-- 배포용 레포지토리(stable) 업데이트 시, 실행에 필수적인 진입점 파일(`brain.py`)이 포함되어 있는지 반드시 확인해야 함.
-- 동기화 도구(`sync_stable.py`)의 제외 목록(`EXCLUDE_FILES`)을 수정할 때는 서비스 코어 파일이 포함되지 않도록 주의 필요.
diff --git a/docs/Bug/20260419_attachment_button_fix.md b/docs/Bug/20260419_attachment_button_fix.md
new file mode 100644
index 0000000..adef133
--- /dev/null
+++ b/docs/Bug/20260419_attachment_button_fix.md
@@ -0,0 +1,28 @@
+# 💡 지식 작성기(Composer) 파일 첨부 버튼 누락 및 모바일 UI 개선
+
+- **날짜**: 2026-04-19
+- **작성자**: Antigravity (AI Agent)
+
+## 1. 개요
+지식 작성기(Composer)에서 드래그 앤 드롭 방식 외에 파일을 직접 선택할 수 있는 물리적 버튼이 없어 접근성이 낮음. 특히 모바일 환경에서 아이콘만 있을 경우 조작 오탐점이 높고 가시성이 떨어지는 문제 발생.
+
+## 2. 조치 사항
+- **UI 개선**:
+ - `templates/components/composer.html` 내부에 클립(📎) 아이콘 버튼 및 숨겨진 `input[type="file"]` 추가.
+ - 모바일 해상도(768px 이하)에서만 나타나는 텍스트 레이블("파일추가") 추가.
+- **다국어 지원(i18n)**:
+ - `static/locales/ko.json`, `en.json`에 `composer_attach_file`, `composer_attach_label` 키 추가.
+- **로직 최적화**:
+ - `static/js/editor.js`: 드롭 이벤트와 버튼 클릭 이벤트를 공통으로 처리하기 위한 `handleFiles()` 함수 추출.
+ - `static/js/components/ComposerManager.js`: 신규 버튼과 파일 인풋 간의 이벤트 바인딩 처리.
+- **스타일링**:
+ - `static/css/layout.css`: 기본 레이블 숨김 처리.
+ - `static/css/mobile.css`: 모바일 환경에서 레이블 노출 및 터치 영역 최적화.
+
+## 3. 결과 및 검증
+- 홈 서버 배포 완료.
+- 데스크톱: 툴팁 지원 및 깔끔한 아이콘 UI 확인.
+- 모바일: 텍스트 레이블 노출로 조작 편의성 증대 확인.
+
+## 4. 향후 주의 사항
+- `action-btn` 스타일 변경 시 모바일에서의 `min-width`, `min-height`가 `44px` 이상 유지되는지 확인 필요 (Apple/Google UI 가이드라인 준수).
diff --git a/static/css/layout.css b/static/css/layout.css
index 8a6fa42..3795199 100644
--- a/static/css/layout.css
+++ b/static/css/layout.css
@@ -104,6 +104,7 @@
}
.action-btn:hover { background: rgba(184, 59, 94, 0.8); }
+.btn-label { display: none; }
.ai-btn:hover { background: var(--ai-accent); color: white; }
/* Memo Footer Metadata Styling */
diff --git a/static/css/mobile.css b/static/css/mobile.css
index de89a95..1653eac 100644
--- a/static/css/mobile.css
+++ b/static/css/mobile.css
@@ -33,6 +33,7 @@
.memo-actions { opacity: 1 !important; bottom: 8px; right: 8px; }
.action-btn { padding: 8px 12px; font-size: 1rem; min-width: 44px; min-height: 44px; display: flex; align-items: center; justify-content: center; }
+ .btn-label { display: inline; margin-left: 5px; font-size: 0.9rem; font-weight: bold; }
.modal-content { width: 95%; max-height: 90vh; border-radius: 12px; }
.tag-badge, .group-badge { padding: 4px 10px; font-size: 0.85rem; }
diff --git a/static/js/components/ComposerManager.js b/static/js/components/ComposerManager.js
index 6e89f42..b9a3c31 100644
--- a/static/js/components/ComposerManager.js
+++ b/static/js/components/ComposerManager.js
@@ -28,7 +28,9 @@ export const ComposerManager = {
foldBtn: document.getElementById('foldBtn'),
discardBtn: document.getElementById('discardBtn'),
deleteBtn: document.getElementById('deleteMemoBtn'), // NEW
- categoryBar: document.getElementById('composerCategoryBar')
+ categoryBar: document.getElementById('composerCategoryBar'),
+ attachBtn: document.getElementById('attachBtn'),
+ fileInput: document.getElementById('composerFileInput')
};
if (!this.DOM.composer || !this.DOM.trigger) return;
@@ -71,6 +73,18 @@ export const ComposerManager = {
this.DOM.encryptionToggle.onclick = () => this.toggleEncryption();
this.initShortcutHint();
+ // 💡 파일 첨부 버튼 연동
+ if (this.DOM.attachBtn && this.DOM.fileInput) {
+ this.DOM.attachBtn.onclick = () => this.DOM.fileInput.click();
+ this.DOM.fileInput.onchange = (e) => {
+ const files = e.target.files;
+ if (files.length > 0) {
+ EditorManager.handleFiles(files);
+ e.target.value = ''; // 같은 파일 다시 올릴 수 있게 초기화
+ }
+ };
+ }
+
// 2. 자동 임시저장 및 키보드 리스너 등록
this.draftTimer = setInterval(() => this.saveDraft(), 3000);
ComposerDraft.checkRestore((draft) => this.restoreDraft(draft));
diff --git a/static/js/editor.js b/static/js/editor.js
index f07100c..c18e67b 100644
--- a/static/js/editor.js
+++ b/static/js/editor.js
@@ -147,37 +147,46 @@ export const EditorManager = {
if (!files || files.length === 0) return;
// 에디터가 닫혀있다면 상위에서 열어줘야 함
- onDropComplete(true);
+ if (onDropComplete) onDropComplete(true);
- for (let file of files) {
- try {
- const data = await API.uploadFile(file);
- if (data.url) {
- const filename = data.url.split('/').pop();
- const isImg = ['png','jpg','jpeg','gif','webp','svg'].includes(data.ext?.toLowerCase());
- const name = data.name || 'file';
-
- // Ensure editor is focused before inserting
- this.editor.focus();
-
- if (isImg) {
- this.editor.exec('addImage', { altText: name, imageUrl: data.url });
- }
-
- // 공통: 첨부 파일 목록에 추가 및 UI 갱신
- this.attachedFiles.push({
- filename: filename,
- original_name: name,
- file_type: file.type
- });
- this.sessionFiles.add(filename); // 세션 트래킹 추가
- this.refreshAttachmentUI();
- }
- } catch (err) { console.error(err); }
- }
+ await this.handleFiles(files);
});
},
+ /**
+ * 다중 파일을 처리(업로드 및 UI 반영)함
+ */
+ async handleFiles(files) {
+ if (!files || files.length === 0) return;
+
+ for (let file of files) {
+ try {
+ const data = await API.uploadFile(file);
+ if (data.url) {
+ const filename = data.url.split('/').pop();
+ const isImg = ['png','jpg','jpeg','gif','webp','svg'].includes(data.ext?.toLowerCase());
+ const name = data.name || 'file';
+
+ // Ensure editor is focused before inserting
+ this.editor.focus();
+
+ if (isImg) {
+ this.editor.exec('addImage', { altText: name, imageUrl: data.url });
+ }
+
+ // 공통: 첨부 파일 목록에 추가 및 UI 갱신
+ this.attachedFiles.push({
+ filename: filename,
+ original_name: name,
+ file_type: file.type
+ });
+ this.sessionFiles.add(filename); // 세션 트래킹 추가
+ this.refreshAttachmentUI();
+ }
+ } catch (err) { console.error('[Editor] File process error:', err); }
+ }
+ },
+
getAttachedFilenames() {
return this.attachedFiles.map(f => f.filename);
},
diff --git a/static/locales/ko.json b/static/locales/ko.json
index b58dd8d..34a77bf 100644
--- a/static/locales/ko.json
+++ b/static/locales/ko.json
@@ -23,6 +23,8 @@
"composer_save": "메모 저장",
"composer_discard": "취소 (삭제)",
"composer_encrypt": "암호화",
+ "composer_attach_file": "파일 첨부",
+ "composer_attach_label": "파일추가",
"composer_password": "비밀번호",
"tooltip_fold": "창 접기 (내용 보존)",
diff --git a/templates/components/composer.html b/templates/components/composer.html
index 8825635..e9adcb5 100644
--- a/templates/components/composer.html
+++ b/templates/components/composer.html
@@ -17,6 +17,10 @@
+
+