fix: 코드앱 서버 모드 전면 수리 — localDirHandle 가드 12곳이 기능을 막고 있었다
서버 워크스페이스 모드는 파일 I/O만 모드별로 갈라놨고, 프로젝트 단위
연산과 프롬프트 구성은 여전히 localDirHandle(로컬 폴더 핸들)을 요구하고
있었다. 서버 모드에선 그 값이 null이라 해당 기능이 조용히 죽었다.
가장 눈에 띈 증상은 "코드가 채팅에 나온다"였는데, 원인은 프롬프트였다.
저장소 안내가 로컬 폴더만 알아서, 서버 워크스페이스가 멀쩡히 붙어
있는데도 모델에게 "로컬 폴더가 연결되지 않았습니다"라고 알려주고 있었다.
저장할 곳이 없다고 들으면 모델은 Write를 안 쓰고 코드를 본문에 뱉는다.
현재 파일 내용을 싣는 블록도 같은 가드에 막혀 있어서, Edit이 맞출 원문을
못 본 채 전체를 다시 출력하는 쪽으로 흘렀다.
"프로젝트 디렉토리가 오락가락한다"는 별개였다. Read/Write/Edit는 베어
이름을 받아 _clientProjectPath로 접두사를 붙이는데 Glob/Grep만 전체
경로를 돌려줘서, 모델이 한 대화 안에서 두 이름 체계를 섞어 봤다. Glob이
준 `otherproj/x.py`를 Write에 넘기면 _clientBareFileName은 활성 프로젝트
접두사만 떼므로 `myproj/otherproj/x.py`로 중첩됐다. 목록이 활성 프로젝트로
범위도 안 잡혀서 남의 프로젝트로 새기도 했다. _projectScopedFiles로
활성 프로젝트 안 + 베어 경로로 통일했다.
"새 세션" 버튼은 부수효과가 물음보다 먼저였다. 히스토리 비우기가 prompt
위에 있어서 취소해도 대화가 이미 날아갔고, 이름 없이 빠져나오면 새 빈
세션이 옛 프로젝트에 묶여 파일이 옛 폴더로 들어갔다. 먼저 묻고, 취소
(null)와 빈 입력('')을 구분한다.
나머지 고친 곳: 프로젝트 생성/삭제/전환, _clientLoadProjectFiles,
폴더 삭제(확인 창조차 안 떴다), 폴더 목록(항상 "파일 없음"), 폴더뷰
파일 삭제, HTML 미리보기(CSS/JS 안 붙은 반쪽), 폴더 이름 바꾸기(빈 원본
디렉토리가 디스크에 남았다).
서버엔 대응 API가 아예 없어서 둘을 추가했다: POST /api/code/mkdir,
DELETE /api/code/dir. 디렉토리 삭제는 파일 삭제(/api/code/file)와 일부러
분리했다 — 한 엔드포인트에 재귀 삭제를 얹으면 파일 하나 지우려던 호출이
오타 하나로 프로젝트를 통째로 날린다. 루트 삭제와 경로 탈출은 403.
Bash 툴과 companion 실행은 손대지 않았다. 서버 파일은 사용자 PC에 없어서
구조적으로 로컬 전용이다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,50 @@ export function registerCodeRoutes(app: express.Application): void {
|
||||
}
|
||||
});
|
||||
|
||||
// 서버 모드 프로젝트 폴더 만들기.
|
||||
// 로컬 모드는 File System Access API의 getDirectoryHandle({create:true})로 빈 폴더를
|
||||
// 만들 수 있는데 서버 쪽엔 대응물이 없어서, 새 프로젝트 생성이 로컬 모드에서만 동작했다
|
||||
// (2026-08-16 리포트). /api/code/save는 파일을 쓸 때만 부모 디렉토리를 만들기 때문에
|
||||
// "빈 프로젝트를 먼저 만들고 파일은 나중에" 흐름이 서버 모드에선 성립하지 않았다.
|
||||
app.post('/api/code/mkdir', (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
const rel = String(req.body?.path || '').trim();
|
||||
if (!rel) { res.status(400).json({ success: false, error: 'path required' }); return; }
|
||||
const codeDir = getCodeDir(req);
|
||||
const target = path.resolve(codeDir, rel);
|
||||
if (target !== codeDir && !target.startsWith(codeDir + path.sep)) {
|
||||
res.status(403).json({ success: false, error: 'Access denied' }); return;
|
||||
}
|
||||
fs.mkdirSync(target, { recursive: true });
|
||||
res.json({ success: true, path: rel });
|
||||
} catch (e: any) {
|
||||
res.status(500).json({ success: false, error: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
// 디렉토리 삭제는 파일 삭제(/api/code/file)와 일부러 분리했다. 같은 엔드포인트에 재귀
|
||||
// 삭제를 얹으면 파일 하나 지우려던 호출이 오타 하나로 프로젝트를 통째로 날릴 수 있다.
|
||||
app.delete('/api/code/dir', (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
const rel = String(req.query.name || '').trim();
|
||||
if (!rel) { res.status(400).json({ success: false, error: 'name required' }); return; }
|
||||
const codeDir = getCodeDir(req);
|
||||
const target = path.resolve(codeDir, rel);
|
||||
// 루트 자체를 지우려는 요청은 막는다 — 워크스페이스 전체가 날아간다.
|
||||
if (target === codeDir || !target.startsWith(codeDir + path.sep)) {
|
||||
res.status(403).json({ success: false, error: 'Access denied' }); return;
|
||||
}
|
||||
if (!fs.existsSync(target)) { res.status(404).json({ success: false, error: 'Not found' }); return; }
|
||||
if (!fs.statSync(target).isDirectory()) {
|
||||
res.status(400).json({ success: false, error: 'Not a directory — use /api/code/file' }); return;
|
||||
}
|
||||
fs.rmSync(target, { recursive: true, force: true });
|
||||
res.json({ success: true, path: rel });
|
||||
} catch (e: any) {
|
||||
res.status(500).json({ success: false, error: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Code editor: scaffold a new project (dirs + files) ───────────────────
|
||||
app.post('/api/code/scaffold', (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
|
||||
+132
-40
@@ -185,9 +185,10 @@ function initCodeView() {
|
||||
}
|
||||
|
||||
function codeNewFile(name, content) {
|
||||
// In client-local + project mode: tabs always store bare filenames; the project
|
||||
// subfolder prefix is handled transparently by _clientProjectPath at disk I/O time.
|
||||
// In server mode with activeProjectName: prefix IS part of the tab name (existing behaviour).
|
||||
// 탭 이름은 언제나 베어 파일명이고, 프로젝트 접두사는 디스크 I/O에서 _clientProjectPath가
|
||||
// 붙인다 — 로컬/서버 모드 구분 없이 동일하다. (예전 주석은 "서버 모드에서는 접두사가 탭
|
||||
// 이름의 일부"라고 적혀 있었는데, 지금 코드와 맞지 않아 경로 버그를 쫓을 때 오히려
|
||||
// 방해가 됐다. 모델에게 보이는 이름도 전부 베어로 통일돼 있다 — _projectScopedFiles 참고.)
|
||||
let fname = name;
|
||||
if (!fname) {
|
||||
const raw = prompt('파일 이름:', 'untitled.py');
|
||||
@@ -417,6 +418,36 @@ async function codeServerDelete(filename) {
|
||||
return !!d?.success;
|
||||
} catch { return false; }
|
||||
}
|
||||
// 프로젝트 단위 연산. 로컬 모드는 getDirectoryHandle({create:true}) / removeEntry({recursive})로
|
||||
// 되는데 서버엔 대응물이 없어서, 새 프로젝트 생성·삭제가 로컬 모드에서만 동작했다(2026-08-16).
|
||||
async function codeServerMkdir(dirPath) {
|
||||
try {
|
||||
const d = await api('/api/code/mkdir', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: dirPath }),
|
||||
});
|
||||
return !!d?.success;
|
||||
} catch { return false; }
|
||||
}
|
||||
async function codeServerDeleteDir(dirPath) {
|
||||
try {
|
||||
const d = await api('/api/code/dir?name=' + encodeURIComponent(dirPath), { method: 'DELETE' });
|
||||
return !!d?.success;
|
||||
} catch { return false; }
|
||||
}
|
||||
// 저장소 종류를 감추는 래퍼 — 호출부는 어느 모드인지 신경 쓰지 않는다.
|
||||
async function projectMkdir(name) {
|
||||
if (codeIsServerMode()) return await codeServerMkdir(name);
|
||||
if (!localDirHandle) return false;
|
||||
try { await localDirHandle.getDirectoryHandle(name, { create: true }); return true; }
|
||||
catch (e) { console.warn('[projectMkdir]', e); return false; }
|
||||
}
|
||||
async function projectRemoveDir(name) {
|
||||
if (codeIsServerMode()) return await codeServerDeleteDir(name);
|
||||
if (!localDirHandle) return false;
|
||||
try { await localDirHandle.removeEntry(name, { recursive: true }); return true; }
|
||||
catch (e) { console.warn('[projectRemoveDir]', e); return false; }
|
||||
}
|
||||
|
||||
function codeUpdateStorageBtn() {
|
||||
const b = document.getElementById('code-storage-btn');
|
||||
@@ -525,7 +556,7 @@ function _clientBareFileName(fname) {
|
||||
// Close all open tabs that belong to the client-local project scope,
|
||||
// then load every code file from the new activeProjectName subfolder.
|
||||
async function _clientLoadProjectFiles() {
|
||||
if (!localDirHandle || !activeProjectName) return;
|
||||
if (!codeStorageReady() || !activeProjectName) return;
|
||||
|
||||
// Close all existing tabs (they belonged to the previous project)
|
||||
if (codeFiles.length) {
|
||||
@@ -537,7 +568,7 @@ async function _clientLoadProjectFiles() {
|
||||
}
|
||||
|
||||
// Ensure the session's directory exists on disk (new session → no folder yet)
|
||||
await localDirHandle.getDirectoryHandle(activeProjectName, { create: true }).catch(() => {});
|
||||
await projectMkdir(activeProjectName);
|
||||
|
||||
const allFiles = await localListFiles(localDirHandle, '');
|
||||
const projFiles = allFiles.filter(f =>
|
||||
@@ -1838,7 +1869,10 @@ function codeRunWith(mode) {
|
||||
|
||||
if (mode === 'browser') {
|
||||
const ext = file.name.split('.').pop().toLowerCase();
|
||||
if ((ext === 'html' || ext === 'htm') && localDirHandle) {
|
||||
// CSS/JS 인라인은 _openHtmlWithInlinedAssets가 localReadFile로 읽어오는데, 그 함수는
|
||||
// 이미 서버/로컬을 갈라준다. 여기서 localDirHandle을 요구할 이유가 없었고, 그래서
|
||||
// 서버 모드에서는 링크된 CSS/JS가 빠진 반쪽 미리보기가 열렸다.
|
||||
if ((ext === 'html' || ext === 'htm') && codeStorageReady()) {
|
||||
// Inline linked CSS and JS so the blob URL can resolve them
|
||||
_openHtmlWithInlinedAssets(file).then(url => {
|
||||
const w = window.open(url, '_blank');
|
||||
@@ -2862,7 +2896,10 @@ async function _codeAiSendImpl() {
|
||||
|
||||
// Full-file context (Edit tool needs exact text to match)
|
||||
let execContextMsg = contextMsg;
|
||||
if (currentFile && monacoEditor && localDirHandle) {
|
||||
// Edit 툴은 원문과 정확히 일치하는 텍스트가 있어야 동작한다. 이 블록이 localDirHandle로
|
||||
// 막혀 있어서 서버 모드에선 현재 파일 내용이 아예 안 실렸고, 모델은 고칠 원문을 못 본 채
|
||||
// 코드를 통째로 다시 뱉는 쪽으로 흘렀다(2026-08-16).
|
||||
if (currentFile && monacoEditor && codeStorageReady()) {
|
||||
const fullContent = monacoEditor.getValue();
|
||||
const ext = currentFile.name.split('.').pop() || '';
|
||||
const displayName = _clientBareFileName(currentFile.name);
|
||||
@@ -3084,6 +3121,21 @@ function _globMatch(pattern, filepath) {
|
||||
return new RegExp('^' + re + '$').test(filepath);
|
||||
}
|
||||
|
||||
// 모델에게 보여줄 파일 목록 — **활성 프로젝트 안으로 한정하고 베어 경로로 돌려준다.**
|
||||
//
|
||||
// 2026-08-16 "프로젝트 디렉토리가 오락가락한다"의 정체. Read/Write/Edit는 베어 이름을 받아
|
||||
// _clientProjectPath로 접두사를 붙이는데, Glob/Grep만 localListFiles의 **전체 경로**를 그대로
|
||||
// 돌려주고 있었다. 그래서 모델은 한 대화 안에서 두 가지 이름 체계를 섞어 보게 되고,
|
||||
// Glob이 준 `otherproj/x.py`를 Write에 넘기면 _clientBareFileName은 활성 프로젝트 접두사만
|
||||
// 떼므로 `myproj/otherproj/x.py`처럼 엉뚱한 곳에 중첩돼 써졌다.
|
||||
// 게다가 목록에 다른 프로젝트 파일까지 다 나와서 모델이 남의 프로젝트로 흘러가기도 했다.
|
||||
async function _projectScopedFiles() {
|
||||
const all = await localListFiles(localDirHandle, '');
|
||||
if (!activeProjectName) return all;
|
||||
const prefix = activeProjectName + '/';
|
||||
return all.filter(f => f.startsWith(prefix)).map(f => f.slice(prefix.length));
|
||||
}
|
||||
|
||||
async function _executeToolLocal(name, args) {
|
||||
try {
|
||||
switch (name) {
|
||||
@@ -3146,8 +3198,7 @@ async function _executeToolLocal(name, args) {
|
||||
case 'Glob': {
|
||||
const pattern = args.pattern || '**/*';
|
||||
if (!codeStorageReady()) return { result: '(저장 위치가 설정되지 않았습니다 — 로컬 폴더 또는 서버 모드)', error: false };
|
||||
const allFiles = await localListFiles(localDirHandle, '');
|
||||
const matches = allFiles.filter(f => _globMatch(pattern, f));
|
||||
const matches = (await _projectScopedFiles()).filter(f => _globMatch(pattern, f));
|
||||
return { result: matches.length ? matches.join('\n') : '(매칭 파일 없음)', error: false };
|
||||
}
|
||||
case 'Grep': {
|
||||
@@ -3159,9 +3210,8 @@ async function _executeToolLocal(name, args) {
|
||||
if (searchPath) {
|
||||
files = [_clientBareFileName(searchPath)];
|
||||
} else {
|
||||
files = await localListFiles(localDirHandle, '');
|
||||
const CODE_EXTS_RE = /\.(js|ts|jsx|tsx|py|java|c|cpp|h|cs|go|rs|php|rb|swift|kt|html|css|json|yaml|yml|xml|md|sh|sql)$/i;
|
||||
files = files.filter(f => CODE_EXTS_RE.test(f));
|
||||
files = (await _projectScopedFiles()).filter(f => CODE_EXTS_RE.test(f));
|
||||
}
|
||||
let regex;
|
||||
try { regex = new RegExp(pattern, ci ? 'gi' : 'g'); } catch { regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), ci ? 'gi' : 'g'); }
|
||||
@@ -3326,10 +3376,15 @@ function _buildCodeSystemPrompt() {
|
||||
⚠️ 코드 작업은 이 파일들을 대상으로 하세요. 로컬 폴더를 Glob/list해서 프로젝트를 파악하려 하지 마세요.\n`;
|
||||
}
|
||||
|
||||
// 로컬 폴더 노트 (파일 저장 경로)
|
||||
const localNote = localDirHandle
|
||||
// 저장소 노트 (파일 저장 경로). 서버 모드를 여기 안 넣어둬서, 워크스페이스가 멀쩡히
|
||||
// 연결돼 있는데도 모델에게 "로컬 폴더가 연결되지 않았습니다"라고 알려주고 있었다.
|
||||
// 저장할 곳이 없다고 들으면 모델은 Write 툴을 안 쓰고 코드를 채팅에 뱉는다
|
||||
// (2026-08-16 "코드 나왔네" 리포트의 유력한 원인).
|
||||
const localNote = codeIsServerMode()
|
||||
? `서버 워크스페이스가 파일 저장소로 연결되어 있습니다${activeProjectName ? ` (프로젝트 "${activeProjectName}")` : ''}. Write/Edit 툴로 파일을 만들고 고치세요 — 코드를 채팅 본문에 붙여넣지 마세요.`
|
||||
: localDirHandle
|
||||
? `로컬 폴더 "${localDirHandle.name}"이 파일 저장소로 연결되어 있습니다.`
|
||||
: '로컬 폴더가 연결되지 않았습니다.';
|
||||
: '파일 저장소가 연결되지 않았습니다.';
|
||||
|
||||
const karpathyBlock = _karpathyMode ? `
|
||||
## Karpathy 코딩 가이드라인 (필수)
|
||||
@@ -4502,10 +4557,8 @@ async function deleteActiveProject() {
|
||||
_knownProjectsRemove(name);
|
||||
localStorage.removeItem('code_proj_session_' + name);
|
||||
localStorage.removeItem('code_proj_history_' + name);
|
||||
// 로컬 폴더 삭제
|
||||
if (localDirHandle) {
|
||||
try { await localDirHandle.removeEntry(name, { recursive: true }); } catch (e) { console.warn('[deleteProject]', e); }
|
||||
}
|
||||
// 프로젝트 폴더 삭제 (서버/로컬 공통)
|
||||
await projectRemoveDir(name);
|
||||
// 탭 닫기 및 프로젝트 초기화
|
||||
codeFiles.forEach(f => { try { f.model?.dispose(); } catch {} });
|
||||
codeFiles.length = 0; activeCodeFileId = null; codeDiffState = {};
|
||||
@@ -4552,8 +4605,9 @@ function switchProject(name) {
|
||||
_renderSessionsList();
|
||||
}
|
||||
}
|
||||
// In client-local mode: close previous project's tabs and load new project files
|
||||
if (localDirHandle) _clientLoadProjectFiles();
|
||||
// 이전 프로젝트 탭을 닫고 새 프로젝트 파일을 연다. 저장소 종류와 무관하다 —
|
||||
// localDirHandle로 막아두면 서버 모드에서 프로젝트를 바꿔도 파일이 안 열렸다.
|
||||
if (codeStorageReady()) _clientLoadProjectFiles();
|
||||
}
|
||||
|
||||
function csbRenderProjectSelector(projects) {
|
||||
@@ -4717,7 +4771,39 @@ function _deleteAllSessions(event) {
|
||||
updateSessionPanel();
|
||||
}
|
||||
|
||||
// "↺ 새 세션" 버튼. 이름을 넣으면 새 프로젝트를 만들고, 비우면 현재 프로젝트에서 세션만
|
||||
// 새로 시작한다.
|
||||
//
|
||||
// 2026-08-16 "새 프로젝트를 만들어도 자꾸 기존 프로젝트로 간다" 리포트로 두 가지를 고쳤다:
|
||||
//
|
||||
// 1) 부수효과가 물음보다 먼저였다. 히스토리 비우기와 /api/clear-history가 prompt 위에
|
||||
// 있어서, **취소를 눌러도 대화가 이미 날아간 뒤**였다. 그리고 이름 없이 빠져나온 뒤
|
||||
// 맨 아래에서 `saveProjectSession(activeProjectName)`이 새 빈 세션을 **옛 프로젝트에**
|
||||
// 묶어버려, 새로 시작한 줄 알고 쓰면 파일이 옛 프로젝트 폴더로 들어갔다.
|
||||
// 이제 먼저 묻고, 정해진 다음에만 건드린다.
|
||||
// 2) 취소(null)와 빈 입력('')을 구분한다. 취소는 아무것도 하지 않는 게 맞다 —
|
||||
// 예전엔 둘 다 "현재 프로젝트에서 세션 초기화"로 흘러서 취소가 파괴적이었다.
|
||||
async function codeAiClear() {
|
||||
if (!codeStorageReady()) {
|
||||
alert('저장소가 연결되지 않았습니다. 로컬 폴더를 열거나 상단 버튼으로 서버 모드로 바꿔주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = prompt(
|
||||
'새 프로젝트 이름을 입력하세요.\n(비워 두고 확인하면 현재 프로젝트에서 세션만 새로 시작합니다)',
|
||||
'');
|
||||
if (raw === null) return; // 취소 — 아무것도 건드리지 않는다
|
||||
|
||||
const projName = raw.trim().replace(/[^a-zA-Z0-9가-힣_-]/g, '_').replace(/^_+|_+$/g, '');
|
||||
|
||||
// 새 프로젝트를 만들 때는 폴더 생성이 성공한 뒤에야 기존 상태를 정리한다.
|
||||
if (projName) {
|
||||
if (!await projectMkdir(projName)) {
|
||||
alert(`⚠️ "${projName}" 폴더를 만들지 못했습니다. 저장소 연결을 확인해주세요.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_autoSaveChatSession(); // 현재 세션 자동 저장
|
||||
if (activeProjectName) saveProjectSession(activeProjectName); // per-project store도 지우기 전에 저장
|
||||
fetch('/api/clear-history', {
|
||||
@@ -4727,18 +4813,14 @@ async function codeAiClear() {
|
||||
}).catch(() => {});
|
||||
codeAiHistory = []; _prevTurnDoneTools = []; _readFileCache = {}; _historySummary = '';
|
||||
|
||||
// In client-local mode: ask for a project name and create the folder
|
||||
if (localDirHandle) {
|
||||
const raw = prompt('새 프로젝트 이름:', '');
|
||||
const projName = (raw || '').trim().replace(/[^a-zA-Z0-9가-힣_-]/g, '_').replace(/^_+|_+$/g, '');
|
||||
if (projName) {
|
||||
await localDirHandle.getDirectoryHandle(projName, { create: true }).catch(() => {});
|
||||
switchProject(projName);
|
||||
codeAiAppend('system', `📁 프로젝트 "${projName}" 생성됨 (빈 폴더). 파일은 Write 툴로 만드세요. Bash는 companion 연결 시에만 사용 가능합니다. 어떤 걸 만들지 설명해주세요.`);
|
||||
return;
|
||||
}
|
||||
if (projName) {
|
||||
// switchProject가 activeProjectName을 새 이름으로 바꾸고 세션·탭·트리를 다시 잡는다.
|
||||
switchProject(projName);
|
||||
codeAiAppend('system', `📁 프로젝트 "${projName}" 생성됨 (빈 폴더). 파일은 Write 툴로 만드세요. Bash는 companion 연결 시에만 사용 가능합니다. 어떤 걸 만들지 설명해주세요.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 이름을 안 줬다 = 현재 프로젝트에서 세션만 새로 시작
|
||||
codeAiSessionId = 'code_ai_' + Math.random().toString(36).slice(2, 10);
|
||||
if (activeProjectName) saveProjectSession(activeProjectName);
|
||||
updateSessionPanel();
|
||||
@@ -5759,12 +5841,19 @@ async function csbDeleteFile(filename, ev) {
|
||||
|
||||
async function csbDeleteDir(dirPath, ev) {
|
||||
ev.stopPropagation();
|
||||
if (!localDirHandle || !confirm(`"${dirPath}" 디렉토리와 내부 파일을 모두 삭제할까요?`)) return;
|
||||
// localDirHandle로 막혀 있어서 서버 모드에선 클릭해도 아무 반응이 없었다(2026-08-16).
|
||||
// 확인 창조차 안 떠서 "버튼이 죽었나" 싶은 모양이었다.
|
||||
if (!codeStorageReady() || !confirm(`"${dirPath}" 디렉토리와 내부 파일을 모두 삭제할까요?`)) return;
|
||||
try {
|
||||
const parts = dirPath.split('/').filter(Boolean);
|
||||
let parent = localDirHandle;
|
||||
for (let i = 0; i < parts.length - 1; i++) parent = await parent.getDirectoryHandle(parts[i]);
|
||||
await parent.removeEntry(parts[parts.length - 1], { recursive: true });
|
||||
if (codeIsServerMode()) {
|
||||
// 트리 경로는 프로젝트 접두사를 포함한 워크스페이스 상대경로라 그대로 넘긴다.
|
||||
if (!await codeServerDeleteDir(dirPath)) throw new Error('서버가 삭제를 거부했습니다');
|
||||
} else {
|
||||
const parts = dirPath.split('/').filter(Boolean);
|
||||
let parent = localDirHandle;
|
||||
for (let i = 0; i < parts.length - 1; i++) parent = await parent.getDirectoryHandle(parts[i]);
|
||||
await parent.removeEntry(parts[parts.length - 1], { recursive: true });
|
||||
}
|
||||
// 해당 디렉토리 아래 열려 있는 탭 모두 닫기
|
||||
const prefix = dirPath.endsWith('/') ? dirPath : dirPath + '/';
|
||||
for (const f of [...codeFiles]) {
|
||||
@@ -5866,10 +5955,10 @@ async function csbRenameExec(oldPath, newName, isDir) {
|
||||
const content = await localReadFile(f);
|
||||
if (content !== null) await localWriteFile(newPath + '/' + f.slice(prefix.length), content);
|
||||
}
|
||||
// 서버에는 디렉토리 삭제 API가 없다. 원본 파일을 하나씩 지우면 빈 디렉토리만 남는데,
|
||||
// 목록은 파일 기준이라 트리에서는 사라진다. 로컬은 기존대로 핸들로 통째 삭제.
|
||||
// 예전엔 서버에 디렉토리 삭제 API가 없어서 파일만 하나씩 지웠고, 빈 원본 디렉토리가
|
||||
// 디스크에 그대로 남았다(트리는 파일 기준이라 안 보였을 뿐). 이제 /api/code/dir이 있다.
|
||||
if (codeIsServerMode()) {
|
||||
for (const f of allFiles.filter(f => f.startsWith(prefix))) await localDeleteFile(f);
|
||||
await codeServerDeleteDir(oldPath);
|
||||
} else {
|
||||
const ps = oldPath.split('/');
|
||||
let parent = localDirHandle;
|
||||
@@ -6356,7 +6445,9 @@ async function codeRefreshFolder() {
|
||||
if (!list) return;
|
||||
list.innerHTML = '<div style="padding:8px 12px;color:var(--muted);font-size:11px">로딩 중...</div>';
|
||||
try {
|
||||
const files = localDirHandle ? await localListFiles(localDirHandle, '') : [];
|
||||
// localDirHandle 유무로 갈라서 서버 모드에선 늘 빈 배열이었다 → 목록이 "파일 없음"으로만
|
||||
// 떴다. localListFiles가 이미 모드별로 갈라주므로 준비 여부만 보면 된다.
|
||||
const files = codeStorageReady() ? await localListFiles(localDirHandle, '') : [];
|
||||
if (!files.length) {
|
||||
list.innerHTML = '<div style="padding:8px 12px;color:var(--muted);font-size:11px">파일 없음</div>';
|
||||
return;
|
||||
@@ -6398,7 +6489,8 @@ async function codeOpenFromFolder(filename) {
|
||||
|
||||
async function codeDeleteFromFolder(filename, ev) {
|
||||
ev.stopPropagation();
|
||||
if (!localDirHandle || !confirm(`"${filename}" 파일을 삭제할까요?`)) return;
|
||||
// 서버 모드에선 이 가드에 걸려 확인 창조차 안 떴다 — 클릭해도 무반응.
|
||||
if (!codeStorageReady() || !confirm(`"${filename}" 파일을 삭제할까요?`)) return;
|
||||
try {
|
||||
const ok = await localDeleteFile(filename);
|
||||
if (ok) {
|
||||
|
||||
Reference in New Issue
Block a user