Skip to content

feat(desktop): 폴더 없는 상태의 새 문서 길과 최근 파일 - #22

Merged
Bori-github merged 18 commits into
mainfrom
feat/m9-no-folder
Aug 9, 2026
Merged

feat(desktop): 폴더 없는 상태의 새 문서 길과 최근 파일#22
Bori-github merged 18 commits into
mainfrom
feat/m9-no-folder

Conversation

@Bori-github

@Bori-github Bori-github commented Aug 3, 2026

Copy link
Copy Markdown
Owner

요약 / 의도

폴더를 열지 않고 앱을 켜도 새 문서 작성과 파일 열기가 되게 한다. 빈 상태(탭 0개) 화면에 버튼(폴더 열기 · 파일 열기 · 새 문서)을 두고, 문서 모델이 규정만 하고 구현이 없던 recentFiles를 세션에 실어 저장·복원해 사이드바에서 다시 열게 한다.

범위

이번에 한 것

  • Session.recent_files — 세션 파일에 최근 파일 목록 저장, 부팅 시 파일 단위 허용 루트 등록(탭 경로와 동일 규칙)
  • show_save_dialogstart_dir — 폴더가 열려 있으면 저장 다이얼로그가 그 루트에서 시작. macOS에서 rfd가 시작 위치와 파일명을 동시에 못 받아 무효화되던 것을 우회(아래 트러블슈팅)
  • 프론트 recentFiles 상태 — 파일 열기·저장 다이얼로그의 새 경로 확정이 canonical 경로를 올림(맨 앞 삽입·중복 이동·상한 10), 세션 복원은 목록 불변
  • 빈 상태 화면 — 버튼(폴더 열기가 첫 자리, 새 문서는 Cmd+N과 같은 Untitled 길) + 단축키의 키별 키캡 표기
  • 최근 파일을 사이드바 영역으로 — 사이드바를 파일 트리 · 최근 파일 · 설정으로 구분하고, 최근 파일은 헤더로 접기(접으면 하단 고정, 접힘 상태 비영속) · 트리와 같은 방향키 탐색(Tab 정지점 하나)
  • 모션 — 상태 색 전환(버튼·탭), 접기/펼치기 크기 전환(사이드바 · 최근 파일 · 트리 폴더 — 언마운트 없이 숨김 상태로 전환), 다이얼로그 진입·퇴장. 종류·공통 규칙·예외는 decisions/motion.md(신설)가 소유
  • 실앱 E2E 하네스 안정화 — 확정 키는 대상 요소에 합성 KeyboardEvent 디스패치, 전환 완료 대기는 전환을 끄고 최종 상태 단언(실측 한계 2건을 testing.md에 기록)
  • 실앱 E2E 데모 감속 — NORII_E2E_SLOWMO_MS가 동작(클릭·입력·훅 트리거) 앞에서만 쉬어 데모 영상을 사람이 볼 속도로 녹화한다(대기 폴링 제외, 일반 실행 불변). 최근 파일 헤더 접기 단계도 시나리오에 추가
  • 경로 이름 헬퍼 2벌(document-store·sidebar)을 shared/lib entryNameOf로 통합(백로그 소화 — 셋째 사본 방지)

의도적으로 안 한 것 / 후속

  • 새 문서의 실파일 즉시 생성 — 폴더가 없으면 만들 자리가 없어 이 상태에서 성립하지 않음. 기존 Untitled 모델(첫 저장 다이얼로그가 경로 확정)을 채택
  • vault 레지스트리·시작 마법사 — 파일 우선 모델 유지(설계 논의로 확정, 비목표 문서와 일치)
  • 앱 내 이름 변경·삭제 시 최근 파일 목록의 즉시 보정 — 없는 경로는 다음 부팅의 load_session이 걸러냄. 세션 중 stale 항목 클릭은 기존 열기 실패 안내를 탐
  • 접힘 상태(최근 파일 헤더)의 영속화 — 파일 생명주기 정책의 사이드카 config가 필요해질 때의 후속

주요 변경점

  • Rust(src-tauri): session.rsrecent_files(serde 기본값 — 필드 없는 옛 세션도 읽힘) · dialog_commands.rs에 시작 디렉터리(존재하는 디렉터리일 때만)
  • 프론트(FSD): workspace 스토어 recentFiles+noteRecentFile/setRecentFiles · open-file/save-tab에서 갱신 · session-storage 스냅숏·복원 연결 · widgets/editor-pane/ui/empty-state.tsx 신설 · widgets/sidebarrecent-files-section 신설(+recent-section-store), 사이드바·트리에 접기/펼치기 전환 · shared/ui dialog에 닫힘 전환 시퀀스
  • 문서(.claude): document-model(최근 파일 절 신설·빈 탭 절 확장·렌더 범위) · rust-commands(Session·다이얼로그 계약) · decisions/motion.md 신설(+DESIGN.md 링크) · testing.md 성숙도 주의 · development-commands(데모 감속 사용법)

설계 · 결정

  • 결정/근거: 최근 파일의 영속화 자리는 session.json이 구조상 강제 — 재시작 뒤 다이얼로그 없이 다시 열려면 부팅 시 허용 루트 등록이 필요하고 그 유일한 통로가 load_session이다. 새 문서 버튼은 Cmd+N과 같은 길(계약 변경 없음), 폴더 열기를 첫 자리에 둔다(강제하지 않음)
  • 접기/펼치기는 언마운트하지 않는다 — 크기 전환이 성립하려면 접힌 쪽이 DOM에 남아야 하고, 숨김 상태(aria-hidden·visibility)로 포커스·탐색에서 뺀다(→ decisions/motion.md)
  • 문서와 달라진 점: 없음 — 계약 문서를 먼저/같은 커밋에서 갱신. PR 전 수동 대조로 계약 8곳을 코드와 대조해 낡은 서술 20건을 문서 쪽에서 수정(코드 변경 없음), 대조 시점 규정도 /pr 절차에 명시

검증 (체크가 아니라 값으로)

  • mise run check: 통과 (프론트 537 테스트 · Rust 83 테스트 · docs-drift 포함)
  • TDD: 예 — Rust recent_files 필터·옛 세션 호환, 스토어 삽입·상한, 열기/저장 갱신, 복원 불변, 사이드바 접기·최근 파일 영역·방향키 탐색을 테스트로 먼저 고정
  • 실앱 E2E: 21/21 통과 (6a82382 상태에서 전체 실행). 저장 다이얼로그 시작 위치·포커스 링은 네이티브라 실앱 수동 확인(아래 트러블슈팅·스크린샷)
  • 번들 크기: N/A (빌드 산출물 필요 — check에 없음)
  • 자동 검증 불가 항목: 빈 상태의 파일 열기·폴더 열기 버튼 클릭과 저장 다이얼로그 시작 위치는 네이티브 다이얼로그라 자동화 불가 — 수동 확인: 앱에서 버튼 클릭 → 다이얼로그가 뜨는지, 폴더 연 상태에서 Cmd+S(Untitled) → 다이얼로그가 그 폴더에서 시작하는지. 포커스 링 실물(잘림 없음)은 시각 확인

관련 문서 (단일 출처)

  • .claude/docs/document-model.md — #최근-파일(신설) · #빈-탭--탭바는-비지-않는다(확장) · 파일 트리 렌더 범위
  • .claude/docs/rust-commands.md — Session 구조체 · 경로 종류 표 · show_save_dialog 시그니처
  • .claude/docs/design/decisions/motion.md — 적용된 모션의 종류·공통 규칙·예외(신설)
  • .claude/docs/testing.md — #성숙도-주의(E2E 실측 한계 추가)
  • .claude/docs/implementation-plan.md — M9 완료 · 백로그 1건 소화

리뷰 포인트 / 위험 지점

  • session.rsrecent_files 허용 등록 — 파일 단위라 스코프 확대는 없으나 보안 관점 확인
  • session.json 호환(serde 기본값) — 필드 없음 → 빈 목록, 세션 유실 없음(테스트로 고정)
  • 접기/펼치기의 언마운트 없음 — 접힌 내용이 DOM에 남으므로 숨김 상태 처리(aria-hidden·탐색 제외)가 빠지면 스크린리더·키보드에 노출된다
  • 다이얼로그 닫힘이 animationend에 걸려 있다 — 유실 대비 타이머 폴백 포함
  • 예시 문서(examples/)는 대상 아님 — .md 파일 하나로 시연되지 않는 앱 상태 표면(빈 상태·세션·사이드바)

트러블슈팅 — 저장 다이얼로그 시작 위치 (rfd 우회)

  • 증상: 폴더를 연 상태에서 새 문서를 저장하면 다이얼로그가 열린 폴더가 아니라 OS 마지막 위치에서 시작한다. 계약(rust-commands.mdshow_save_dialog·start_dir)과 어긋난다.
  • 원인: norii와 tauri-plugin-dialog는 set_directory·set_file_name을 표준대로 넘긴다. rfd 0.16.0의 build_save_filestarting_directoryfile_name을 함께 set_path로 넘기고, set_path가 시작 폴더에 파일명을 이어붙여(/폴더/파일명) 존재하지 않는 경로를 setDirectoryURL(isDirectory:true)로 넣어 NSSavePanel이 무시한다. rfd master에도 같은 코드가 있어 버전 상향으로는 해결되지 않는다.
  • 해결: start_dir이 있으면 set_file_name을 생략해 rfd가 파일명을 붙이지 못하게 한다.
  • 트레이드오프: start_dir이 있으면 파일명 기본값(default_name)이 제안되지 않는다. rfd가 둘을 동시에 받지 못해 시작 위치를 택했다. 근본 원인은 rfd에 있어 업스트림 수정 시 우회를 걷어낸다.
  • 검증 한계: 네이티브 저장 다이얼로그라 자동 E2E로 검증할 수 없다.

체크리스트 (프로젝트 규칙)

  • 동작을 바꾸는 변경은 계약 문서를 먼저/같은 커밋에서 갱신
  • docs-drift 통과 (tech-stack 버전 ↔ 핀, Rust 커맨드 등재)
  • 비목표 경계(.claude/rules/non-goals.md)를 넘지 않음
  • 성능 규칙 준수 (새 상시 비용 없음 — 목록은 상한 10, 전환은 상호작용 트리거만)
  • 커밋·PR 제목이 Conventional Commits(한국어)

스크린샷 / 데모

E2E 시나리오를 그대로 녹화한 것이다(동작마다 1초 감속).

빈 상태 — 버튼이 뜨고 새 문서 버튼이 Untitled 탭을 연다

norii-demo-empty-state.mov

최근 파일 — 닫은 파일이 사이드바 목록에 남고 누르면 다시 열리고, 헤더로 접힌다

norii-demo-recent-files.mov

사이드바 — 폴더 트리 펼침·파일 열기, 접기 토글, 만들기·이름 변경·삭제

norii-demo-sidebar.mov

세션 복원 — 다시 띄우면 열려 있던 탭이 돌아온다

norii-demo-session.mov

마일스톤 / 비고

M9. 폴더 없는 상태 — 계획 표의 산출물(폴더 없이 새 문서를 만들어 저장까지, 최근에 연 파일을 그 자리에서 다시 열기)을 충족. 설계 방향(파일 우선 유지)은 사전 논의로 확정.

🤖 Generated with Claude Code

@Bori-github Bori-github added the feat 기능 추가/변경 label Aug 3, 2026
Bori-github and others added 6 commits August 5, 2026 12:10
recent_files는 tabs[].path와 같은 규칙으로 걸러진다 — 파일만, 존재하는
경로만, 읽을 때 파일 단위 허용 루트로 등록. 필드가 없는 옛 session.json은
빈 목록으로 읽어 기존 세션을 잃지 않는다. 갱신 시점·상한 정책은
document-model.md#최근-파일이 소유한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
show_save_dialog에 start_dir를 추가한다. 존재하는 디렉터리면 다이얼로그
시작 위치로 쓰고, 아니면 OS 기본(마지막 사용 위치)이다. 웹뷰는 사이드바
rootDir를 넘기고, 폴더를 열지 않았으면 null이다. 시작 위치는 허용 루트
등록과 무관하다 — 등록은 사용자가 선택한 경로로만 한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
workspace 스토어에 recentFiles를 둔다 — 맨 앞 삽입, 중복은 앞으로 이동,
상한 10. 파일 열기(새 탭·기존 탭 합류)와 저장 다이얼로그의 새 경로 확정이
canonical 경로를 올린다. 세션 복원은 저장된 목록을 그대로 세우고 순서를
바꾸지 않는다. 표시(빈 상태 안내)는 다음 커밋이 맡는다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
document-store의 fileNameOf와 sidebar의 folderNameOf는 같은 구현이었다.
shared/lib(path-segments)로 옮겨 탭 제목·사이드바 헤더·최근 파일 목록이
같은 규칙으로 이름을 만들게 한다. 백로그의 해당 항목을 지운다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
폴더를 열지 않고 앱을 켰을 때 이 화면에서 문서 작업을 시작한다. 폴더
열기 · 파일 열기 · 새 문서 버튼을 이 순서로 두고, 새 문서 버튼은 Cmd+N과
같은 Untitled 길을 탄다. 최근 파일 목록은 파일명으로 표시하고 누르면 그
파일을 연다 — 비면 표시하지 않는다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
빈 상태 행동 버튼 셋의 존재·순서, 새 문서 버튼의 Untitled 길(활성 탭
경로 없음), 닫은 파일이 최근 파일 목록에 남고 눌러 다시 열리는 흐름,
세션 파일의 recentFiles 기록을 실앱에서 검증한다(21/21 통과). 파일
열기·폴더 열기 버튼 클릭은 네이티브 다이얼로그라 수동 검증 대상이다.
구현 계획의 M9 상태와 문서 링크를 갱신한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bori-github and others added 11 commits August 6, 2026 13:49
폴더 열기는 accent 버튼 하나로 남기고, 파일 열기·새 문서는 테두리 없는
ghost 행으로 바꾼다 — 이름은 왼쪽, 단축키는 오른쪽의 고정폭 칩이다.
최근 파일 행도 같은 ghost 행이고 카드 폭을 60에서 80으로 넓힌다. 모양을
서술하던 주석은 지우고 기능 제약(투명 창 배경)만 남긴다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
단축키를 악센트 배경의 키캡으로 그린다 — 아랫변 테두리만 2px라 눌린
깊이가 생기고, 한 글자가 키캡 하나다(문자열은 STRINGS 그대로, 렌더에서
나눈다). 키마다 최소 폭을 줘 글자가 달라도 크기가 균일하다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
사이드바를 파일 트리·최근 파일·설정 영역으로 나눈다. 최근 파일 영역은
헤더로 접고 펼치며, 접으면 헤더만 하단(설정 위)에 남고, 목록이 비면
표시하지 않는다. 목록은 Tab 정지점 하나이고 방향키·Home/End로 항목을
옮긴다(트리와 같은 규칙). 항목 버튼의 포커스 링이 스크롤 면 가장자리에서
잘리지 않게 목록 안쪽 여백을 링보다 넓게 둔다. 빈 상태 화면의 목록은
제거한다 — 표시 자리는 사이드바 한 곳이다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
isDisplayed()가 긴 시퀀스 중 보이는 요소에 false를 주는 간헐 어긋남을
실측했다(DOM은 정상, 창 최상위·포커스·숨김 상태와 무관). 설정 창의 표시
단언을 DOM(hidden)으로 바꾼다. 세션 복원의 리로드 대기는 리로드 전
화면이 같은 단언을 만족해 그대로 통과하던 것을, 리로드 직전 표식을 심고
표식 없는 새 페이지에서 훅 존재까지 확인하는 방식으로 바꾼다. 두 한계는
testing.md#성숙도-주의가 소유한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
버튼·탭의 배경/테두리/글자색에 fast·out 전환을 걸고, 동작 줄이기에서는 전환을 끈다.
적용된 모션의 종류·공통 규칙·예외는 decisions/motion.md가 소유하고 DESIGN.md가 링크한다.
"장식적 모션을 두지 않는다"는 "상태 변화를 설명하는 것만 둔다"와 같은 말이라 삭제한다.
탭 호버 테스트는 전환 도입으로 즉시 단언이 깨져 완료 대기로 바꾼다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
사이드바(폭)·최근 파일·트리 폴더(grid 행)를 언마운트 없이 크기 전환으로 여닫는다.
접힌 쪽은 숨김 상태(aria-hidden·visibility)로 두고 키보드 탐색·Tab 정지점에서 뺀다.
트리 폴더의 첫 펼침은 접힘으로 마운트한 뒤 타이머로 펼친다(예외 근거는 decisions/motion.md).
document-model.md에 접힌 children이 DOM에 남는 렌더 범위를 기록한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
닫힘은 dialogOut 전환이 끝난 뒤 close()·언마운트하고, animationend 유실은 타이머 상한으로
막는다. 동작 줄이기에서는 전환 없이 즉시 닫는다. 확인 다이얼로그는 닫힘 전환 동안 마지막
내용을 유지한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
browser.keys()의 확정 Enter가 세션의 활성 요소로 가서 간헐 실패하던 것을 대상 요소에
합성 KeyboardEvent를 디스패치하는 방식으로 바꾼다. 렌더링되지 않는 창에서 CSS 전환이
진행되지 않아 완료 대기가 간헐 실패하던 것은 전환을 끄고 최종 상태(폭·테두리 0)를 즉시
단언한다. 두 실측 한계를 testing.md 성숙도 주의에 기록하고, 사이드바 접힘·다이얼로그
닫힘 대기를 시나리오에 추가한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
데모 녹화가 기계 속도라 사람이 볼 수 없어 NORII_E2E_SLOWMO_MS로 동작(클릭·입력·훅
트리거) 앞에서만 쉬게 한다 — 대기 폴링에는 걸지 않아 일반 실행은 그대로다.
최근 파일 헤더 접기는 새 표면인데 실앱 E2E 커버가 없어 기존 최근 파일 시나리오에
단계를 더한다. 전체 스위트 일반 속도 21/21 통과.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
수동 대조 시점이 "마일스톤 종료 시"로만 있어 PR 생성 때 수행하는지가 규정에 없었다.
모든 PR 전에 대조하고 어긋남 수정을 그 PR에 담는 것으로 정하고, /pr 커맨드 절차에
단계를 추가한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR 전 수동 대조에서 확인한 불일치를 문서 쪽에서 수정한다(코드 변경 없음).

- rust-commands: 세션 커맨드 2개를 async fn으로, 미구현 plugin-window-state 행 제거
- document-model: 세션 저장 파일 표에서 창 크기·위치 행 제거(도입 여부는 열린 결정으로),
  재펼침은 캐시 사용, WorkspaceState를 실제 형태로(expandedDirs 필드, 탭은 별도 스토어),
  최근 파일 추가 시점에 사이드바 새 파일 만들기 경로 추가
- editor-strategy: 단축키 표에 3행 추가(탭 Delete/Backspace 닫기 · 탭바 방향키 전환 ·
  전체 접기/펼치기), 확인 다이얼로그 중 전역 단축키 정지 명시
- preview-strategy: 뷰 모드는 설정 파일에 저장·복원(열린 결정 문구 삭제), 복사 버튼
  노출 조건에 키보드 포커스·복사됨 상태 포함
- window-chrome: 36px의 _glass 한정 삭제, docs-drift 상수 대조와 모순되는 "자동 검사는
  이것뿐" 삭제, 렌더 검증은 Vitest Browser Mode로 가능함을 반영, 부팅 ①②는 Promise.all
- file-lifecycle: 충돌 UI를 배너로 통일, 상태 다이어그램에서 코드에 없는 error 상태 제거,
  접힘 상태는 저장하지 않음을 명시
- mise verify-native 설명에 한글 조합 확정 입력 포함

데이터를 물건처럼 쓰는 표현(버린다·올린다·되살린다·저장 자리)도 동작 서술로 고치고,
문서를 인용하는 주석 2곳(open-file·E2E 집행)을 같이 맞춘다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Bori-github Bori-github self-assigned this Aug 8, 2026
rfd가 set_directory와 set_file_name을 함께 받으면 시작 위치를 시작폴더/파일명으로 합쳐
존재하지 않는 경로로 만들어 무효화한다 — 그래서 폴더를 연 상태에서 저장 다이얼로그가
열린 폴더가 아니라 OS 마지막 위치에서 시작했다. 시작 위치를 줄 때는 파일명을 붙이지
않아 다이얼로그가 열린 폴더에서 시작하게 한다.

트레이드오프: start_dir이 있으면 파일명 기본값(default_name)이 제안되지 않는다. rfd가
둘을 동시에 받지 못하므로 시작 위치를 택했다. 근본 원인은 rfd에 있어 업스트림 수정 시
우회를 걷어낸다.

네이티브 저장 다이얼로그라 자동 검증이 불가해 실앱에서 수동 확인했다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Bori-github Bori-github changed the title feat(desktop): 폴더 없는 상태의 새 문서 길과 최근 파일 (M9) feat(desktop): 폴더 없는 상태의 새 문서 길과 최근 파일 Aug 9, 2026
@Bori-github
Bori-github merged commit 57e405d into main Aug 9, 2026
1 check passed
@Bori-github
Bori-github deleted the feat/m9-no-folder branch August 13, 2026 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 추가/변경

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant