본문으로 건너뛰기
AI PR Review Toolkit

라이브 데모

2026. 5. 13.

feat(extension,host): v0.3.2 — 폴더 우선 정렬, 드래그 가능한 splitter, ZIP 압축

Tab Explorer v0.3.2 PR — folder-first sort, resizable splitters, ZIP compression. Claude AI가 작성한 한국어 PR 리뷰·설명 dogfood 결과.

local-fx claude-sonnet-4-6 ₩0 (Claude Max)

이슈 분포 + 소요 시간

0
CRITICAL
5
WARNING
8
SUGGESTION
4분 54초
소요 시간

PR 설명 자동 생성

PR 제목

feat(extension,host): v0.3.2 — 폴더 우선 정렬, 드래그 가능한 splitter, ZIP 압축

설명

기존 Tab Explorer는 파일 리스트가 이름 순으로만 정렬되어 폴더와 파일이 알파벳 순으로 뒤섞였고, 좌측 드라이브 패널과 하단 상태바가 고정 폭/높이라 사용자가 화면을 자기 작업에 맞게 조정할 수 없었습니다. 또한 v0.3.1까지는 호스트에 압축 기능이 없어 ZIP 생성을 위해 매번 외부 도구로 빠져나가야 했습니다. v0.3.2는 이 세 가지 UX 격차를 한 릴리스로 묶어 해결하여 Windows 탐색기 수준의 기본 조작감을 맞추는 것을 목표로 합니다. 해결 방식은 영역별로 분리했습니다. 정렬은 호스트 측 `sortEntries`에 디렉터리 우선 1차 키(`dirRank`)를 추가해 사용자가 고른 desc 옵션과 무관하게 폴더가 항상 위에 오도록 했습니다. Splitter는 controlled 컴포넌트(`Splitter.tsx`) + Pointer Capture로 구현하고, 너비/높이는 별도 `layout` zustand store에 두어 300ms 디바운스 + `beforeunload` flush로 localStorage 영속화했습니다. ZIP 압축은 호스트의 streaming op로 신규 추가(`compress.go`)하여 copy/move와 동일한 progress/done 이벤트 envelope을 재사용하고, UTF-8 헤더 플래그(0x800)로 한글 파일명을 보존합니다.

체인지로그

## [0.3.2] - 2026-05-13

### Added
- 파일 리스트에 디렉터리 우선 2단계 정렬을 도입해 Windows 탐색기와 동일하게 폴더가 항상 파일보다 위에 오도록 했습니다.
- 좌측 드라이브 패널과 하단 상태바 사이에 드래그·키보드·더블클릭 리셋이 가능한 splitter를 추가했습니다. 조절값은 localStorage에 영속화됩니다.
- 호스트에 streaming `compress` op를 추가하고, 행 컨텍스트 메뉴에 "ZIP으로 압축" 항목을 노출했습니다. 단일 선택은 `<이름>.zip`, 복수 선택은 `archive_YYYYMMDD_HHMMSS.zip`으로 자동 명명합니다.
- 진행률 토스트와 결과 모달에 압축(compress) 종류를 통합했습니다.
- 신규 에러 코드 `E_ARCHIVE_FAILED`를 추가했습니다.

### Changed
- `App.css`의 메인 레이아웃 grid를 CSS 변수 기반으로 전환해 splitter 드래그가 즉시 반영되도록 했습니다.
- ProgressToasts의 종류 라벨을 `kindLabel` switch로 리팩토링했습니다(향후 종류 추가 시 누락 방지).
- 확장 `manifest.json`/`package.json` 및 네이티브 호스트 버전을 0.3.2로 올렸습니다.

### Fixed
- 압축 작업 실패 모달 제목이 "이동 결과"로 잘못 표기되는 경로를 차단했습니다(FailureSummary 큐 분기 누락 수정).

한 줄 요약

폴더 우선 2단 정렬, 드래그 가능한 splitter, streaming 기반 ZIP 압축 op를 한 릴리스에 묶은 견고한 기능 PR — 호스트 단위 테스트는 두텁지만 압축 op의 archiveName 검증·메모리 효율·실패 시나리오 커버에 보강 여지가 있다. **CRITICAL**: 이 변경에서 즉시 데이터 손상·보안 사고로 이어지는 항목은 발견하지 못했다. 아래 WARNING #1은 "현 UI에서는 도달 불가"라 격하했지만 IPC 직접 호출을 허용하는 구조라면 CRITICAL로 승급 검토 권장.

WARNING

5
WARNING native-host/internal/ops/compress.go:117-127
archiveName 검증이 \x00/\\와 길이만 본다. archiveName = ".." / "." / 끝이 점·공백인 Windows 예약 패턴이 통과된다. - 근거: filepath.Join(destClean, "..")destClean의 부모로 정규화되므로, CheckMutatingOp(destDir)가 통과한 권한 경계 의 경로가 archivePath로 산출될 수 있다. 실제로는 결과가 디렉터리라 os.Create가 EISDIR로 실패해 파일이 쓰이진 않지만, E_BAD_REQUEST 대신 일반 IO 에러로 노출돼 진단이 흐려진다. UI는 현재 archiveName을 노출하지 않아 도달 불가이지만 IPC를 직접 호출하면 트리거 가능. - 수정 예시: ```go if archiveName == "." || archiveName == ".." { return protocol.ErrorResponse(req.ID, protocol.ErrCodeBadRequest, "archiveName must not be '.' or '..'", false) } // 추가: Windows 예약 이름 (CON, PRN, NUL, COM1..LPT9), 끝 공백/점도 거절 ```
WARNING native-host/internal/ops/compress.go (내부 walk 콜백)
파일마다 buf := make([]byte, copyBufSize)로 64KB를 새로 할당한다. - 근거: 폴더가 수천 개 파일을 포함하면 N × 64KB의 단명 객체가 생겨 GC 압력이 커진다. copy.go의 copyBufSize를 재사용하는 패턴과도 일관성이 떨어진다. - 수정 예시: Compress 함수 진입부에서 buf := make([]byte, copyBufSize)를 한 번 할당하고 walk 콜백 클로저에서 캡처해 재사용. 동시성 없음(같은 op 안에서 직렬 진행).
WARNING native-host/internal/ops/compress.go (pre-walk 블록)
bytesTotal/fileTotal 산출을 위해 전체 트리를 두 번 traverse한다. - 근거: 대형 폴더(예: node_modules 수십만 파일)에서 메타데이터 I/O가 두 배가 되고, pre-walk 중 사라진 파일을 본 walk가 다시 보지 못해 fileTotal 불일치(progress 100% 미달)가 생긴다. critic의 W2와 같은 사안. - 완화안: pre-walk를 옵션으로 두거나, 첫 progress emit까지의 latency가 길어도 본 walk와 합치는 단일-패스 구조로 변경. 또는 pre-walk는 빠른 추정(stat sample)으로 줄이고 progress UI는 "indeterminate" 상태 허용.
WARNING native-host/internal/ops/compress.go (모든 `_ = emit(...)` 호출부)
emit 반환 에러를 일괄 무시한다. - 근거: Chrome native messaging 파이프가 끊기면 호출자는 응답을 받지 못하는데, 호스트는 walk를 끝까지 돌면서 디스크 I/O를 계속한다. 취소 시그널이 cancel context가 아닌 stdin close로 들어오는 환경에서 무용한 작업이 길어진다. - 수정 예시: emit 에러 발생 시 canceled = true; return filepath.SkipAll로 walk 조기 종료, cleanup 호출 후 protocol.ErrorResponse(...) 반환.
WARNING native-host/internal/ops/compress.go의 `Compress` 함수 전체
약 280줄 단일 함수. - 근거: golden-principles 규칙 5(함수 ≤50줄) 위반. setup 검증 / pre-walk / 본 walk / finalize 4단계가 한 스코프에 있어 추후 매크로 수정(예: 진행률 emit 정책 변경) 시 회귀 위험이 크다. - 수정 예시: validateAndPlan(args) → plan, prewalkTotals(plan), streamArchive(ctx, plan, prog, emit) → failures, canceled, finalize(...) 4 함수로 분리. 테스트는 streamArchive에 집중.

SUGGESTION

8
SUGGESTION extension/src/ui/components/Splitter.tsx
4px hit area는 마우스로도 잡기 빠듯하고 터치는 사실상 불가. ::before pseudo-element로 ±4px 양쪽 padding을 줘 시각 폭은 4px, hit zone은 12px로 분리 권장.
SUGGESTION native-host/internal/ops/compress_test.go
다음 시나리오 누락: - 압축 도중 ctx cancel → done.canceled=true + 부분 zip 삭제 확인 - 트리 내 EACCES 파일이 FailureInfo로 누적되고 walk가 계속됨을 검증 - 다중 파일 압축 시 progress 이벤트가 최소 1회 emit됨을 검증 - 빈 디렉터리(파일 0개)만 압축 → zip에 디렉터리 엔트리만 존재 - archiveName=".." / "." (WARNING #1 검증용) - 시스템 경로 destDir + explicitConfirm=falseE_SYSTEM_PATH_CONFIRM_REQUIRED
SUGGESTION extension/src/ui/store/jobs.ts:332-396
startCompressJobhandle.promise.then 블록과 evt.event === "done" 분기가 모두 completeJob을 호출할 수 있다. 현재는 state 가드(done|failed|canceled)로 중복 호출을 막지만, 호출 순서(done 이벤트가 먼저 vs 응답이 먼저)에 따른 race를 명시 주석으로 박아두면 다음 사람이 안전.
SUGGESTION extension/src/ui/App.tsx:399-419 `handleCompressZip`
선택 항목이 매우 많을 때(예: 10,000개) 사용자 확인 다이얼로그 없음. copy/move와 일관성은 있지만 압축은 파일 생성이라 부담 더 큼. v0.4.x에서 임계치 confirm 검토.
SUGGESTION extension/src/ui/components/ProgressToasts.tsx:15-24 `kindLabel`
default: assertNever(kind) 추가 시 향후 JobKind 확장 시 누락이 컴파일 에러로 잡힌다. 현재도 TS exhaustiveness가 잡지만 런타임 방어로 한 줄 더 둘 가치 있음.
SUGGESTION extension/src/ui/store/layout.ts:177-198
beforeunload 리스너가 모듈 로드 시점에 등록되고 제거 경로가 없다. 확장 컨텍스트에서는 페이지 단위 lifecycle이라 문제 없지만, 단위 테스트에서 store를 import하면 listener가 쌓일 수 있다. 테스트 환경 가드(if (import.meta.env?.MODE !== "test")) 또는 cleanup export 검토.
SUGGESTION native-host/internal/ops/compress.go의 `archivePathEqual`
Windows 분기에서 strings.EqualFold를 쓰는데, walk path와 archivePath 둘 다 같은 filepath.Join/filepath.Clean을 거치므로 바이트 동등성으로도 충분하다. EqualFold는 Unicode case folding 비용이 있어 큰 폴더에서 매 항목 호출되면 미세하게 비용 누적. 그래도 defense-in-depth 가치는 인정.
SUGGESTION native-host/internal/ops/compress.go의 `dedupeSubPaths`
paths[0]이 leading source라는 ENOENT 분기 의존성이 함수 주석에는 명시돼 있지만, 호출부(Compress)에서도 "deduped[0]은 caller가 준 paths[0]이 살아 있다면 그대로"라는 보장이 docstring으로 남으면 좋다(현재는 sort.SliceStable + 원본 순서 복원으로 그렇게 동작).

분할 권장 여부

**분할은 권장하지 않는다.** M13(정렬·splitter)와 M16(ZIP)이 한 PR에 묶여 있지만: - 각 변경의 모듈 경계가 명확(readdir.go / Splitter.tsx + layout.ts / compress.go)해 충돌 영역이 없다. - 호스트와 확장 버전을 0.3.2로 동시에 올려야 하므로 분리해도 결국 같은 릴리스로 묶인다. - 총 변경 라인이 리뷰 가능한 규모(테스트 포함 ~1,800 LOC). 다음 PR에서 압축에 자동 선택 / progress UI 개선이 들어간다면 그건 별 PR로 분리 권장.

칭찬할 점

- **readdir.go의 desc 처리** — "negate가 아니라 operand swap"으로 정렬 안정성을 보존하는 트릭을 dirRank 1차 키 도입에도 그대로 유지한 점이 견고하다. 동률 입력에서 reordering이 안 생긴다. - **compress.go의 setup 가드 이중화** — destDir-in-source와 archivePath-in-source를 둘 다 검사하고, 본 walk에서도 `archivePathEqual` skip을 한 번 더 둔 다층 방어가 깔끔하다. critic 지적을 반영해 W1까지 동일 맥락으로 통합한 흐름이 좋다.