feat(travel-proxy): 앨범 지역 변경 API + 좌표 메타 API + region_map_extra 리팩토링

- PUT /api/travel/albums/{album}/region: 앨범의 지역 변경 (extra 파일 기반)
- PUT /api/travel/regions/{region_id}: 커스텀 지역 이름/좌표 수정 (Phase 2 준비)
- _load_extra/_save_extra 헬퍼 분리, _removes 키로 원본 오버라이드 지원
- regions API: 모든 커스텀 지역 동적 병합 + Point geometry 지원

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 12:51:59 +09:00
parent 728428ce95
commit 1c255152d7
2 changed files with 182 additions and 32 deletions

View File

@@ -77,51 +77,144 @@ def _load_region_map_merged(region_map_path: Path, thumb_root: Path) -> dict:
raise
# 오버라이드 파일 병합
extra_path = thumb_root / "region_map_extra.json"
if extra_path.exists():
try:
with open(extra_path, "r", encoding="utf-8") as f:
extra = json.load(f)
for k, v in extra.items():
if k in region_map:
# 기존 지역에 앨범 추가 (중복 방지)
existing = region_map[k]
if isinstance(existing, list) and isinstance(v, list):
merged = list(dict.fromkeys(existing + v))
region_map[k] = merged
else:
region_map[k] = v
except (json.JSONDecodeError, OSError) as e:
logger.warning("Failed to load region_map_extra: %s", e)
extra = _load_extra(thumb_root)
if extra:
# _removes: 원본에서 제거할 항목 적용
removes = extra.get("_removes", {})
for region_id, remove_albums in removes.items():
if region_id in region_map:
existing = region_map[region_id]
if isinstance(existing, list):
region_map[region_id] = [a for a in existing if a not in remove_albums]
elif isinstance(existing, dict) and "albums" in existing:
existing["albums"] = [a for a in existing["albums"] if a not in remove_albums]
# 나머지 키 병합 (_removes, _regions_meta 제외)
for k, v in extra.items():
if k.startswith("_"):
continue
if k in region_map:
existing = region_map[k]
if isinstance(existing, list) and isinstance(v, list):
merged = list(dict.fromkeys(existing + v))
region_map[k] = merged
else:
region_map[k] = v
return region_map
def _extra_path(thumb_root: Path) -> Path:
return thumb_root / "region_map_extra.json"
def _load_extra(thumb_root: Path) -> dict:
"""RW 오버라이드 파일 로드."""
path = _extra_path(thumb_root)
if path.exists():
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
pass
return {}
def _save_extra(thumb_root: Path, data: dict) -> None:
"""RW 오버라이드 파일 저장."""
path = _extra_path(thumb_root)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def _save_extra_albums(thumb_root: Path, new_folders: List[str]) -> None:
"""신규 발견 폴더를 RW 영역의 오버라이드 파일에 저장."""
extra_path = thumb_root / "region_map_extra.json"
extra = {}
if extra_path.exists():
try:
with open(extra_path, "r", encoding="utf-8") as f:
extra = json.load(f)
except (json.JSONDecodeError, OSError):
extra = {}
extra = _load_extra(thumb_root)
uncategorized = extra.get("미분류", [])
if isinstance(uncategorized, dict):
uncategorized = uncategorized.get("albums", [])
existing = set(uncategorized)
added = [f for f in new_folders if f not in existing]
if added:
uncategorized.extend(added)
extra["미분류"] = uncategorized
try:
with open(extra_path, "w", encoding="utf-8") as f:
json.dump(extra, f, ensure_ascii=False, indent=2)
_save_extra(thumb_root, extra)
logger.info("Saved %d new folders to region_map_extra.json → 미분류", len(added))
except OSError as e:
logger.warning("Cannot write region_map_extra: %s", e)
def move_album_region(
thumb_root: Path,
region_map_path: Path,
album: str,
new_region: str,
) -> dict:
"""
앨범의 지역을 변경. extra 파일에서 이전 지역에서 제거 + 새 지역에 추가.
원본 region_map.json의 매핑을 오버라이드 하기 위해
extra에 _remove 키로 원본에서 제거할 항목을 기록.
Returns: {"album": str, "old_region": str, "new_region": str}
"""
# 현재 병합 맵에서 앨범의 기존 지역 조회
merged = _load_region_map_merged(region_map_path, thumb_root)
old_region = None
for region_id, v in merged.items():
album_list = v if isinstance(v, list) else v.get("albums", [])
if album in album_list:
old_region = region_id
break
extra = _load_extra(thumb_root)
# 1. 기존 지역에서 제거
if old_region:
# extra에 해당 지역의 앨범 목록이 있으면 거기서 제거
if old_region in extra:
old_list = extra[old_region]
if isinstance(old_list, dict):
old_list = old_list.get("albums", [])
if album in old_list:
old_list.remove(album)
extra[old_region] = old_list
# 원본 region_map에 있는 경우 _overrides로 제거 기록
try:
with open(region_map_path, "r", encoding="utf-8") as f:
original = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
original = {}
orig_list = original.get(old_region, [])
if isinstance(orig_list, dict):
orig_list = orig_list.get("albums", [])
if album in orig_list:
removes = extra.setdefault("_removes", {})
remove_list = removes.setdefault(old_region, [])
if album not in remove_list:
remove_list.append(album)
# 2. 새 지역에 추가
new_list = extra.get(new_region, [])
if isinstance(new_list, dict):
new_list = new_list.get("albums", [])
if album not in new_list:
new_list.append(album)
extra[new_region] = new_list
# _removes에서 새 지역의 해당 앨범은 제거 (되돌리기 대응)
if "_removes" in extra and new_region in extra["_removes"]:
if album in extra["_removes"][new_region]:
extra["_removes"][new_region].remove(album)
_save_extra(thumb_root, extra)
logger.info("Moved album '%s': %s%s", album, old_region or "없음", new_region)
return {"album": album, "old_region": old_region or "미분류", "new_region": new_region}
def sync(
travel_root: Path,
thumb_root: Path,