Library workspace upgrades: darkroom viewer, mosaic, geotagging, file explorer

- darkroom-style fullscreen viewer: left info panel, rating/color-label toolbar,
  bottom filmstrip, keyboard nav (Esc / arrows), placeholder on load failure
- thumbnail density slider (contact-sheet) + photo mosaic generator (target -> tiles)
- lighttable-style hover info preview (no click needed)
- map drag & drop geotagging (saved to index only; originals untouched)
- file explorer: parallel drive scan + timeout, create/delete(trash)/move folders;
  index reparent on move and cleanup on delete (single source of truth)
- library: photos-before-videos ordering; drag range select/deselect;
  native image drag disabled so sweep-select works
- responsive sidebar font scaling; no-wrap filter labels; media protocol CORS + video Range

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 14:47:26 +09:00
parent 3e73967c7b
commit d2546f9cbf
25 changed files with 2358 additions and 434 deletions
+104 -3
View File
@@ -85,6 +85,30 @@ export const MESSAGES: Table = {
'profile.analyzing': { ko: '분석 중', en: 'Analyzing' },
'profile.deletePhoto': { ko: '이 사진 삭제', en: 'Delete this photo' },
// 파일 탐색기 (정리 탭 사이드바)
'explorer.title': { ko: '파일 탐색기', en: 'File Explorer' },
'explorer.asSource': { ko: '소스로', en: 'As source' },
'explorer.asOutput': { ko: '출력으로', en: 'As output' },
'explorer.copy': { ko: '복사', en: 'Copy' },
'explorer.copied': { ko: '경로 복사됨', en: 'Path copied' },
'explorer.hint': {
ko: '폴더를 끌어서 경로 칸에 놓거나, 다른 폴더 위에 놓아 이동할 수 있어요.',
en: 'Drag a folder onto a path field, or onto another folder to move it.'
},
'explorer.newFolder': { ko: '새 폴더', en: 'New folder' },
'explorer.delete': { ko: '삭제', en: 'Delete' },
'explorer.newFolderPrompt': { ko: '새 폴더 이름:', en: 'New folder name:' },
'explorer.confirmDelete': {
ko: "'{name}' 폴더를 휴지통으로 보낼까요?",
en: "Move folder '{name}' to the Recycle Bin?"
},
'explorer.confirmMove': {
ko: "'{src}' 을(를) '{dest}' 안으로 이동할까요?",
en: "Move '{src}' into '{dest}'?"
},
'explorer.opFailed': { ko: '작업 실패: {msg}', en: 'Operation failed: {msg}' },
'explorer.loading': { ko: '드라이브 검색 중…', en: 'Scanning drives…' },
// 2. 폴더
'folder.section': { ko: '2. 폴더 선택', en: '2. Select Folders' },
'folder.source': { ko: '정리할 폴더 (소스)', en: 'Folder to organize (source)' },
@@ -171,6 +195,15 @@ export const MESSAGES: Table = {
},
'map.selectHint': { ko: '지도에서 사진을 클릭하세요.', en: 'Click a photo on the map.' },
'map.loading': { ko: '불러오는 중…', en: 'Loading…' },
'map.geotag': { ko: '지오태깅', en: 'Geotagging' },
'map.geotagHint': {
ko: 'GPS가 없는 사진을 지도 위로 끌어다 놓으면 위치가 지정됩니다. (원본 파일은 그대로, 색인에만 저장)',
en: 'Drag a photo without GPS onto the map to set its location. (Saved to the index only; original files unchanged.)'
},
'map.untaggedCount': { ko: '위치 없는 사진 {n}장', en: '{n} without location' },
'map.allTagged': { ko: '모든 사진에 위치가 있습니다.', en: 'All photos already have a location.' },
'map.tagged': { ko: '위치를 지정했습니다.', en: 'Location set.' },
'map.dropHere': { ko: '여기에 놓아 위치 지정', en: 'Drop here to set location' },
// 그룹화 / 자가정화 (Phase 3)
'groups.section': { ko: '유사 사진 그룹 · 자가정화', en: 'Similar groups · Cleanup' },
@@ -239,6 +272,39 @@ export const MESSAGES: Table = {
en: 'No indexed photos. Add a folder and run indexing.'
},
'lib.loadMore': { ko: '더 보기', en: 'Load more' },
'lib.density': { ko: '크기', en: 'Size' },
'lib.densityHint': {
ko: '썸네일 크기 조절 — 작게 하면 폴더 전체를 한눈에(컨택트시트), 크게 하면 자세히 봅니다.',
en: 'Thumbnail size — smaller shows the whole folder at a glance, larger shows detail.'
},
'viewer.back': { ko: '뒤로', en: 'Back' },
'viewer.hint': {
ko: '← → 이동 · Esc 닫기',
en: '← → navigate · Esc to close'
},
'viewer.videoUnsupported': {
ko: '이 영상 형식은 미리보기를 지원하지 않습니다.',
en: 'Preview is not available for this video format.'
},
// 포토모자이크 (대표 사진을 라이브러리 사진들로 재구성)
'panel.mosaic': { ko: '포토모자이크', en: 'Photo mosaic' },
'mosaic.make': { ko: '🧩 이 사진으로 모자이크', en: '🧩 Make mosaic' },
'mosaic.hint': {
ko: '대표 사진을 한 번 클릭(선택)한 뒤 버튼을 누르세요. 라이브러리 사진들이 작은 타일이 되어 그 사진을 재현합니다.',
en: 'Click a target photo to select it, then press the button. Library photos become tiles that recreate it.'
},
'mosaic.needTarget': { ko: '대표 사진을 먼저 선택하세요.', en: 'Select a target photo first.' },
'mosaic.target': { ko: '대상', en: 'Target' },
'mosaic.tiles': { ko: '타일 {n}장', en: '{n} tiles' },
'mosaic.resolution': { ko: '해상도', en: 'Resolution' },
'mosaic.blend': { ko: '색 보정', en: 'Color blend' },
'mosaic.unique': { ko: '중복 줄이기', en: 'Reduce repeats' },
'mosaic.save': { ko: 'PNG 저장', en: 'Save PNG' },
'mosaic.building': { ko: '타일 색상 분석 중…', en: 'Analyzing tile colors…' },
'mosaic.tooFew': {
ko: '타일이 너무 적습니다. 색인된 사진이 많을수록 모자이크가 정교해집니다.',
en: 'Too few tiles. The more indexed photos, the finer the mosaic.'
},
// 선택 / 내보내기 / 삭제 (Library)
'sel.count': { ko: '{n}개 선택', en: '{n} selected' },
@@ -263,9 +329,11 @@ export const MESSAGES: Table = {
'media.video': { ko: '영상', en: 'Videos' },
// 컬링 필터 / 품질 플래그 (Phase 1)
'cull.all': { ko: '품질 전체', en: 'All' },
'cull.candidate': { ko: '잘 나온 사진', en: 'Good shots' },
'cull.rejected': { ko: '걸러낼 사진', en: 'To cull' },
'filter.kind': { ko: '종류', en: 'Type' },
'filter.quality': { ko: '품질', en: 'Quality' },
'cull.all': { ko: '전체', en: 'All' },
'cull.candidate': { ko: '잘나온', en: 'Good' },
'cull.rejected': { ko: '걸러낼', en: 'Cull' },
'cull.help': {
ko: '흐리거나 눈 감은·노출 나쁜 사진을 자동으로 표시해, 잘 나온 사진만 빠르게 고를 수 있게 도와줍니다.',
en: 'Auto-flags blurry / closed-eye / badly-exposed shots so you can quickly keep the good ones.'
@@ -288,6 +356,39 @@ export const MESSAGES: Table = {
'col.year': { ko: '연도', en: 'Year' },
'col.camera': { ko: '카메라', en: 'Camera' },
'col.label': { ko: '색라벨', en: 'Label' },
'col.folder': { ko: '폴더', en: 'Folder' },
// darktable식 3-패널 (라이브러리 워크스페이스)
'panel.library': { ko: '라이브러리', en: 'Library' },
'panel.explorer': { ko: '파일 탐색기', en: 'Folders' },
'panel.collections': { ko: '컬렉션', en: 'Collections' },
'panel.filters': { ko: '필터', en: 'Filters' },
'panel.thresholds': { ko: '품질 임계값', en: 'Quality thresholds' },
'panel.info': { ko: '이미지 정보', en: 'Image information' },
'panel.selection': { ko: '선택', en: 'Selection' },
'panel.actions': { ko: '작업', en: 'Actions' },
'panel.rate': { ko: '평가', en: 'Rating' },
'panel.tagging': { ko: '태깅', en: 'Tags' },
'panel.metadata': { ko: '메타데이터', en: 'Metadata' },
'info.none': { ko: '사진을 클릭하면 정보가 표시됩니다.', en: 'Click a photo to see info.' },
'info.file': { ko: '파일', en: 'File' },
'info.date': { ko: '촬영', en: 'Date' },
'info.camera': { ko: '카메라', en: 'Camera' },
'info.lens': { ko: '렌즈', en: 'Lens' },
'info.exposure': { ko: '노출', en: 'Exposure' },
'info.iso': { ko: 'ISO', en: 'ISO' },
'info.size': { ko: '크기', en: 'Size' },
'info.gps': { ko: '위치', en: 'GPS' },
'info.folder': { ko: '폴더', en: 'Folder' },
'tag.placeholder': { ko: '태그 입력 후 Enter', en: 'Type a tag, press Enter' },
'tag.attachHint': { ko: '선택한 사진(없으면 클릭한 사진)에 부착됩니다.', en: 'Attached to the selection (or the focused photo).' },
'tag.assetTags': { ko: '이 사진의 태그', en: "This photo's tags" },
'meta.title': { ko: '제목', en: 'Title' },
'meta.description': { ko: '설명', en: 'Description' },
'meta.creator': { ko: '작성자', en: 'Creator' },
'meta.save': { ko: '저장', en: 'Save' },
'meta.saved': { ko: '저장됨', en: 'Saved' },
'meta.selectFirst': { ko: '사진을 먼저 클릭하세요.', en: 'Click a photo first.' },
// 메뉴
'menu.file': { ko: '파일', en: 'File' },