Skip to content

[feat] Google Drive 녹음 가져오기와 전사 결과 Drive 동기화 - #13

Merged
m16khb merged 7 commits into
mainfrom
12-google-drive-sync
Oct 11, 2026
Merged

m16khb merged 7 commits into
mainfrom
12-google-drive-sync

Conversation

@m16khb

@m16khb m16khb commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

요약

Galpi 데스크톱 앱에 Google Drive 연동을 추가합니다. 사용자는 건너뛸 수 있는 온보딩에서 Drive를 연결하고, Drive에 있는 녹음을 데스크톱 Google Picker로 골라 전사할 수 있습니다. 전사 결과와 회의록은 내 Drive의 Galpi 폴더에 회의별로 올라갑니다. 같은 회의를 다시 처리하면 기존 파일을 갱신하고, 그사이 Drive에서 파일이 바뀌었으면 덮어쓰지 않고 알립니다. 권한은 drive.file 하나이고, 클라이언트 보안 비밀번호 없이 auth-gateway가 토큰 교환을 대행합니다.

Closes #12

변경 내용

  • Rust 호스트: Drive 연결·해제, Picker 가져오기, 회의 결과 동기화를 담당하는 use case(DriveAccounts)와 포트 3개를 추가했습니다. 포트는 18개, IPC 명령은 7개 늘어 35개가 되었습니다.
    • 회의 키는 Drive에서 가져온 녹음이면 Drive 파일 ID, 아니면 정경화한 원본 경로로 만듭니다. 같은 녹음을 다시 전사해 출력 폴더가 {이름} 2로 바뀌어도 같은 Drive 폴더로 동기화됩니다.
    • 충돌은 Drive appProperties에 기록한 SHA-256과 Drive가 계산한 sha256Checksum을 비교해 판정합니다. 로컬에 Drive 파일 기록을 따로 두지 않습니다.
    • Drive 연결을 끊는 경우는 invalid_grant 하나뿐입니다. auth-gateway가 401을 돌려주면 앱 로그인도 함께 풀립니다. 그 밖의 실패는 해당 Drive 작업만 실패로 끝나며, 앱이 저절로 재시도하지 않습니다.
    • 내려받은 Drive 파일의 로컬 이름은 OS 경로 해석 없이 문자열을 직접 나눠 정합니다. 처음 올린 구현은 이름을 Path로 해석해서, Windows CI에서 a:b?.WAV의 a:를 드라이브 접두어로 보고 버렸습니다(b.WAV). 지금은 /와 \를 모든 OS에서 구분자로 보고, 드라이브 접두어는 일반 문자로 정리하므로 두 OS가 같은 이름을 만듭니다(7232f4a).
  • 화면: 온보딩 카드, 상단 Drive 칩, 설정의 Google Drive 절, 결과 아래 동기화 행을 추가했습니다. Drive에서 가져오기는 진행 카드에서 작업으로 실행되고 작업 취소로 멈출 수 있습니다. 충돌이 나면 Drive 버전 덮어쓰기를 눌러야만 덮어씁니다.
  • 문서: README 두 벌, INSTALL.md, DESIGN.md, docs/ARCHITECTURE.md, AGENTS.md를 갱신했습니다. 결정 기록(ADR) 1건과 주의 사항 4건은 .issueops/에 남겼습니다. 네 번째 주의 사항은 Drive 파일 이름을 Path로 해석하면 Windows에서 결과가 달라진다는 내용입니다.
  • 바뀌지 않은 것: 워커와 JSONL 프로토콜, 기존 IPC 명령 28개의 시그니처, 출력 파일 이름 규칙은 그대로입니다. Drive 업로드는 전사 명령과 분리된 IPC로 실행되므로, 업로드가 실패해도 로컬 결과는 그대로 성공합니다.

확인한 것

  • 실제 Google·auth-gateway·Drive 스모크: 연결, Picker로 녹음 고르기, 내려받기, 업로드, 같은 내용 재동기화(변경 없음), 내용 변경 후 갱신, Drive에서 파일을 바꾼 뒤 충돌 감지, 덮어쓰기, 회의록 업로드, 연결 해제를 순서대로 실행해 모두 기대한 결과를 얻었습니다. 업로드 직후 Drive가 sha256Checksum을 돌려주는 것도 확인했습니다.
  • 자동 테스트: macOS에서 cargo test --all-targets 295개, bun test 233개가 통과했습니다. cargo clippy -D warnings, cargo fmt --check, bun run check도 통과했습니다. Drive 어댑터는 가짜 서버로 gateway 대행 교환, 오류 매핑, multipart 업로드, 스트리밍 다운로드 계약을 검사하고, use case 테스트는 로그인 게이트, 계정 불일치 거부, 취소, 생성·갱신·충돌·실패 격리를 검사합니다.
  • Windows 파일 이름 수정: 이름 정리 테스트에 Windows 경로 구문 4개(C:\x\y.m4a, ..\..\boot.ini, C:, c:.wav)를 더하자 macOS에서도 수정 전 구현이 실패했고(C--x-y.m4a), 수정 뒤 통과했습니다. 이 커밋의 PR CI에서 rust (windows-latest)를 포함한 모든 작업이 통과했습니다.
  • 비밀번호 검사: 커밋 전 미추적 파일까지 포함해 저장소 전체에서 클라이언트 보안 비밀번호 모양의 문자열을 찾았고, 일치하는 문자열은 없었습니다.
  • 화면 QA: Vite 개발 서버에 가짜 Tauri IPC를 붙여 Chromium에서 온보딩, 연결 대기와 취소, 가져오기 카드, 동기화 충돌, 설정 절을 확인했습니다. 기능 결함은 나오지 않았습니다. QA와 구현 리뷰에서 나온 지적은 이 PR에서 고쳤습니다. 긴 이메일이 1024px 창에서 상단 바를 넘던 문제, 버튼이 숨을 때 포커스를 잃던 문제, 브라우저에서 취소한 가져오기가 실패로 보이던 문제, Picker 대기 중 다른 계정으로 다시 연결했을 때 이전 계정의 토큰이 저장되던 경합이 여기에 해당합니다. Windows 파일 이름 수정은 화면을 바꾸지 않아 QA를 다시 하지 않았습니다.

확인하지 못한 것

  • 실제 앱 창(bun run dev)에서 전 과정을 실행하지 못했습니다. 테스트한 컴퓨터에 전사 엔진과 모델이 없어서, 화면은 DOM 테스트와 가짜 IPC 화면 확인으로, 호스트 동작은 실제 Google 스모크로 나눠 검증했습니다.
  • 50MB가 넘는 파일을 내려받는 동안의 메모리 사용량은 측정하지 않았습니다. 다운로드는 청크 단위로 .part 파일에 바로 씁니다.
  • 계정 불일치 거부와 실제 전사 뒤 자동 업로드는 application 테스트로만 확인했습니다.

리뷰 포인트

  • auth-gateway 의존: Drive 연결은 auth-gateway의 /drive/native/token이 배포되어 있어야 동작합니다. 엔드포인트는 이미 배포되어 응답하지만 m16khb-org/auth-gateway#10(PR [feat] auth-gateway Google 로그인이 있어야 Galpi를 사용하도록 변경 #11)은 아직 열려 있습니다. 엔드포인트가 없으면 Drive 연결만 실패하고 나머지 기능에는 영향이 없습니다.
  • 충돌 판정 방식: 로컬 기록 없이 Drive 메타데이터만으로 판정하는 방식(domain/drive.rs의 decide)이 Drive 앱이나 웹에서 파일을 고치는 경우를 충분히 덮는지 봐 주세요.
  • 가져온 파일 이름 규칙: 내려받은 파일 이름이 로컬 회의 폴더, 결과 파일 이름, 새로 만드는 Drive 회의 폴더 이름이 됩니다. Drive 이름에 \가 있으면 마지막 조각만 쓰므로(1팀\주간 회의.m4a → 주간 회의), 이 규칙이 괜찮은지 봐 주세요.
  • 후속 후보로 남긴 QA 지적(낮은 우선순위): 재연결이 필요할 때 동기화 행에 연결 버튼이 없는 점, 1024px에서 온보딩 제목 줄바꿈, 계정 칩과 Drive 칩의 모양이 같은 점입니다.

…in the host

Why: galpi#12. Users keep meeting recordings in Google Drive and want the
transcripts back there without moving files by hand. The host now connects
a Drive account with the drive.file scope, imports one recording picked in
the system browser's desktop Picker, and uploads the meeting's results into
a Galpi folder per meeting.

- domain/drive.rs: MeetingKey (`drive:<fileId>` or FNV-1a of the canonical
  source path, so a re-transcription into `{stem} 2` syncs to the same
  folder), the stateless conflict rule `decide` over appProperties
  galpiSha256 vs Drive sha256Checksum, file-id and token value objects.
- application/drive.rs: DriveAccounts behind three new ports
  (DriveAuthPort, DriveFilesPort, DriveSessionStore); connect/disconnect
  under a store lock, token renewal near expiry, invalid_grant is the only
  failure that forgets the connection, gateway 401 signs the app out.
  Picker tokens are stored only while the same account is still connected.
- AppAccessGate gains bearer()/expire(): the auth-gateway session pays for
  POST /drive/native/token, which holds the OAuth client secret; the source
  carries only the desktop client ID.
- adapters/outbound/drive: consent over the shared browser_sign_in
  loopback/PKCE, gateway-brokered token exchange, Drive v3 REST (multipart
  upload, streamed download into cache/drive-imports, cleaned on failure,
  cancel and startup).
- tauri.rs/composition.rs: 7 commands (35 total); settings and the secret
  store keep the Drive session (Windows entry
  com.m16khb.galpi:drive-session); opener allows drive.google.com.

Tested: cargo test --manifest-path src-tauri/Cargo.toml --all-targets (295
passed, 1 ignored); cargo clippy --all-targets -- -D warnings; cargo fmt
--check; live smoke against real Google, auth-gateway and Drive (connect,
Picker import, upload, unchanged, updated, conflict, overwrite, minutes,
disconnect) recorded in .issueops/evidence/task-12-drive-smoke.md
Not-tested: memory during a 50MB+ download; the full flow inside the app
window (no engine/models on the test machine)
Why: galpi#12. The host's Drive commands need a surface: a way to connect,
a way to bring a recording in from Drive, and a visible result of each
meeting's upload, all following DESIGN.md (SEED tokens only, status as
color plus text, no progress percentages that the download cannot back).

- drive-machine.ts: pure reducer for connection, consent wait, and sync
  state; drive-controller.ts drives it and follows AUTH_REQUIRED back to
  the login screen; a Drive failure after the user's own disconnect does
  not bring the onboarding card back.
- Onboarding card (Drive 연결 / 나중에 / cancel), top-bar Drive chip
  (truncates a long address, full one in its title), settings section,
  and the sync row with per-file lines, Drive 폴더 열기, and an explicit
  Drive 버전 덮어쓰기 on a conflict.
- Drive에서 가져오기 runs as a job on the progress card with 작업 취소; a
  consent declined or Picker closed in the browser shows as cancelled.
  begin() sets the job kind before rendering so the card title follows the
  new job. Audio from Drive carries driveSourceId into the transcription
  request; recording or picking a local file replaces it.
- Focus moves to the control that replaces a hidden Drive button (settings
  when start actions are disabled); empty status lines stay live regions.
- tauri-backend.ts: strict Zod schemas so no Drive token reaches the window.

Tested: bun run check; bun test (233 pass); Chromium via Vite with a fake
Tauri IPC (onboarding, connect wait, chip, import card, sync conflict,
settings; 1024px long-address layout; keyboard focus handoff); combined
web QA (aside-web-qa) with no functional defect
Not-tested: the window inside the real app (no engine/models on the test
machine); prefers-reduced-motion toggled at runtime (no new animation)
Why: galpi#12. Users need to know what Drive access Galpi asks for and
what lands in their Drive; agents need the new ports, commands, and
adapters on the architecture map.

- README.md / README.en.md: Drive connection, import, sync, conflicts,
  and the drive.file scope.
- INSTALL.md: Drive sign-in is handed back to the user like the other
  sign-ins.
- docs/ARCHITECTURE.md, AGENTS.md, adapters/outbound/AGENTS.md: 35 IPC
  commands, 18 ports, the Drive use cases and adapters, and the
  auth-gateway token exchange.

Tested: Not applicable (documentation); bun run check passes
Not-tested: n/a
Why: the IssueOps cycle for galpi#12 publishes its contract and evidence
with the change: the approved plan and its review, the gate ledger, the
verification report (including verify-stage review and QA fixes), and the
live Drive smoke result. The decisions and pitfalls found on the way are
recorded where the next change will look.

- ADR: Drive tokens exchanged by auth-gateway; stateless sync through
  appProperties and sha256Checksum.
- Cautions: set the job kind before rendering the job card; gate CHECKs
  with secret-like arguments are denied, and secret scans must include
  untracked files and match the real value shape; the desktop Picker
  consent's transient Google 500 clears on reload.
- architecture/testing overviews: 18 ports, 35 commands, Drive adapters,
  Drive manual checks and the fake-server vs live smoke boundary.

Tested: issueops gates check --write (12/12)
Not-tested: n/a (records)
@m16khb m16khb added the enhancement New feature or request label Oct 10, 2026
@m16khb m16khb linked an issue Oct 10, 2026 that may be closed by this pull request
13 tasks
@m16khb m16khb self-assigned this Oct 10, 2026
Why: the completion record points at this report; it now names the draft
PR it was published as and what stays out of the cycle (merge, deploy,
cleanup).

Tested: issueops remote verify-artifact (PR #13, base main, enhancement,
m16khb)
Not-tested: n/a (record)
@m16khb
m16khb marked this pull request as ready for review October 11, 2026 03:07
Why: PR #13's rust (windows-latest) job failed
downloaded_names_are_sanitized_and_keep_the_extension: local_name parsed
the Drive file name with std::path::Path, and Windows read "a:" in
"a:b?.WAV" as a drive prefix and dropped it ("b.WAV"). macOS treated "\"
as an ordinary character, so the two OSes disagreed on names like
"C:\x\y.m4a".

- local_name splits the name itself on "/" and "\" on every OS, skips
  empty and "." segments, falls back to "meeting" for "..", and splits
  the extension with the same rule as Path::file_stem.
- paths::sanitized_stem holds the shared sanitize-or-"meeting" rule;
  meeting_stem keeps its behavior and now calls it.
- The test adds Windows path syntax (C:\x\y.m4a, ..\..\boot.ini, C:,
  c:.wav) so the mismatch fails on macOS too.

Tested: cargo test --all-targets (295 passed, 1 ignored); cargo clippy
--all-targets -- -D warnings; cargo fmt --check
Not-tested: Windows locally (verified by PR CI after push)
Why: the reopened cycle (generation 3) publishes its evidence with the
fix, and the pitfall belongs where the next change will look.

- gates.md: G13, downloaded Drive names are OS-independent.
- verification-report.md: cause, RED/GREEN, side effects (cache name,
  local meeting folder and result stem, new Drive meeting folder name),
  review, and why web QA was not rerun.
- Caution: parsing Drive names with Path differs on Windows.

Tested: issueops gates check (13/13)
Not-tested: n/a (records)
@m16khb
m16khb merged commit 0ef05a0 into main Oct 11, 2026
9 checks passed
@m16khb
m16khb deleted the 12-google-drive-sync branch October 11, 2026 03:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Google Drive 녹음 가져오기와 전사 결과 Drive 동기화

1 participant