콘텐츠로 이동

zone 롤아웃 (상권 데이터 검수 · 시딩)

Owner: A (grid/region/zone — 지도 인프라) — zone 패키지는 MSG-234에서 Owner A로 확정된 영역이고, 이번 산출물(시드 데이터·검수 스크립트·계약 테스트 픽스처)도 전부 그 안에 있다. 단 프로덕션 Java 코드 변경은 0이다 — 바뀌는 것은 리소스 파일(seed/zones.json)·테스트(ZoneNamingContractTest + 신규 픽스처)·스크립트·prod 설정 1줄·문서뿐. glossary·status.md·위키 갱신은 공통 문서 작업.

요구사항 정본: docs/prd/MSG-259-prd.md (FR-1~17 + FR-2a·FR-7a, 19건) — 본 스펙은 "어떻게"만 붙인다. 선행: MSG-234(완료, develop merge) — V8 zones 테이블·GET /api/zones·ZoneSeeder·테스트 23건. 기계장치는 전부 있고 데이터가 0행이다(seed/zones.json = []). 상세 결정 이력: docs/spec/MSG-234.md §D1~D8. 드라이런 완료(2026-07-30): scripts/zones-draft/build-candidate.pyzones-candidate.json 17건, validate-zones.py PASS. 경계 조정 실값: 서면 maxGridX 112230→112229, 홍대입구 110376→110375.


개요

디자인이 전 화면에 채택한 "서면 A-14" 표기는 코드가 아니라 데이터가 없어서 안 나온다. MSG-234가 만든 기계장치(테이블·API·시더)에 유명 상권·통칭 30~50건(서울·부산 중심, 지역 제한 없음 — PRD §2 2026-07-31 개정)을 검수해 넣고, 명명 규칙이 FE·Android·iOS로 흩어질 때 어긋나지 않도록 언어 중립 픽스처로 못박고, 용어(구역·표시명)를 glossary에 등재한다. zone 패키지 프로덕션 Java 변경 없음이 원칙 — 시더·API·엔티티는 그대로 쓴다.

배경 · 목표

  • 사용자 관점: 사용자가 "홍대"·"서면"·"광안리"처럼 행정동으로 표현되지 않는 통칭으로 자기 격자를 인지한다. 지금은 전부 행정동 폴백("부산광역시 부산진구 부전동")뿐이다.
  • 목표:
  • 확정 상권 데이터(30~50건)가 zones에 시딩되어 그 사각형 안 격자가 "서면 A-14"로 표시된다.
  • 명명 규칙 검증 수단이 Java 전용 테스트에서 언어 중립 JSON 픽스처로 승격되어, FE/모바일이 같은 파일로 자기 구현을 검증한다.
  • glossary에 "구역(zone)"·"표시명(display name)"이 등재된다 (MSG-234 §D8 미완).
  • 위키 문서(FE 계약 노트·ADR)가 실제 상태와 일치한다.
  • 비목표 (PRD §2): 서버 ZoneNamer 구현 · 격자별 zone 저장 · 폴리곤 zone · 타 시도 공공데이터 일괄 재실행(zone 등재 자체는 지역 제한 없음 — 2026-07-31 개정) · 표시명 역파싱 · 작도 내부 도구 · 장소 검색(MSG-251 소관).

성공 기준 (관찰 가능)

  1. seed/zones.json이 확정본 30~50건이고, scripts/zones-draft/validate-zones.pyPASS한다(26행 캡·겹침·키중복·범위역전 0 — FR-6·FR-7·FR-7a).
  2. 시딩 후 GET /api/zones에 확정본 전건이 등장하고, 서면 사각형 북단·서단 격자의 FE 산술 재현값이 "서면 A-1"이다(FR-9). 재시딩해도 결과가 같다(FR-8, 기존 시더 그대로).
  3. ZoneNamingContractTestsrc/test/resources/fixtures/zone-naming.json에서 케이스를 읽으며, 기존 7건 시나리오가 전부 green을 유지한다(핵심 회귀 기준, FR-14).
  4. 픽스처 파일은 Java 문법이 하나도 없는 순수 JSON이라 FE/모바일이 그대로 파싱해 검증할 수 있고, 위키 zone 표시명 FE 계약 노트가 이 파일을 정본으로 가리킨다.
  5. glossary에 "구역(zone)"·"표시명(display name)" 정의가 있다(FR-16, §D-9 초안 그대로).
  6. zone 패키지 프로덕션 Java 변경 0 · 크로스오너 계약 4종 시그니처 불변 · 전체 테스트 green.
  7. 시딩 전(0행) 동작은 지금과 동일 — 전 화면 행정동 폴백, 에러 없음(FR-12, 기존 테스트가 이미 보증).

결정 (Decisions)

D-1 — 검수 파이프라인 = 4단계 (지라 확정 2026-07-30 승격)

  • 결정: 확정본은 다음 4단계로 만든다.
  • 유명 통칭 목록 임의 선정(30~50개) — 우체국·지구대류 비상권 이름과 안 유명한 이웃 상권(역삼·합정 등)은 목록에서 빼서 겹침을 원천 차단(FR-1). 제안 목록은 build-candidate.pyFAMOUS 딕셔너리(23개), 팀 스크럼에서 확정.
  • 같은 시/도 안에서만 합집합 bbox — "서면역 7번/8번/13번 출구" → "서면" 1건. 시/도가 다르면 동명이라도 합치지 않는다(서울시청+부산시청 = 265km 사각형 방지, FR-2).
  • 공공데이터에 없는 유명지는 수동 작도 — 상가 밀집 기준 데이터라 통칭 유명세와 어긋나는 구멍(가로수길·연남·광복 등, FR-2a). 목록 확정 시 변동 — build-candidate.py가 출력하는 missing 목록이 작업 리스트.
  • 겹침은 침범한 쪽 min/max 정수 조정으로 분리 — 드라이런 실측 2쌍(서면↔전포·홍대입구↔신촌, 각각 한 열 침범). 조정 실값은 build-candidate.pyADJUSTMENTS에 코드로 고정(서면 maxGridX 112229, 홍대입구 110375). 강남↔압구정 겹침은 부분문자열 과매칭이 만든 허상 — 정확 매칭으로 소멸(FR-7).
  • 근거: 드라이런으로 전 단계 실증 완료 — 후보 17건 validate PASS, 26행 캡 초과 0건.

D-2 — priority 전원 0 (지라 확정 승격)

  • 결정: 확정본 전건 priority: 0. 상권 간 서열 기준을 만들지 않는다. §D5(MSG-234)의 priority DESC, zoneKey ASC 결정성은 실수 겹침 시 표시가 안 흔들리는 보험으로만 남긴다 — D-1의 4단계가 겹침 자체를 0으로 만들므로 발동할 일이 없다(FR-7·FR-11).

D-3 — 검증 3층 (지라 확정 승격)

  • 결정: 시딩 데이터 품질은 3층으로 지킨다. Java 테스트 신설 없음 — 각 층이 이미 존재한다.
  • 시딩 전: validate-zones.py PASS 필수(exit 1이면 시딩 금지) — 26행 캡·사각형 겹침·zone_key 중복·min>max 역전 기계 검사(FR-7a).
  • 시딩 시: V8 CHECK 3종(chk_zones_y_range·chk_zones_x_range·chk_zones_row_cap)이 DB 레벨에서 거부.
  • : build-candidate.py가 뱉는 zones-candidate.geojson을 geojson.io에 올려 위치·이름만 육안 확인.

D-4 — 명명 계약 픽스처 = 레포 내 언어 중립 JSON, 위키 노트가 미러 (PRD §8 ①)

  • 결정: src/test/resources/fixtures/zone-naming.json을 신설하고 ZoneNamingContractTest가 이 파일에서 케이스를 읽어 검증하도록 리팩터한다(FR-14). 형식:
{
  "zones": {
    "seomyeon": { "zoneKey": "m234-seomyeon", "name": "m234서면",
                  "minGridY": 1000, "maxGridY": 1010, "minGridX": 2000, "maxGridX": 2020, "priority": 0 },
    "tall":     { "...": "기존 테스트의 m234높은구역 그대로" },
    "big":      { "...": "" }, "hot": { "...": "" }
  },
  "cases": [
    { "name": "북단_서단_격자는_A_1로_명명된다",
      "gridId": "1010_2000", "zones": ["seomyeon"], "regionName": null, "expected": "m234서면 A-1" },
    { "name": "매칭_zone이_없으면_행정동_이름으로_폴백한다",
      "gridId": "9999_9999", "zones": ["seomyeon"], "regionName": "부산광역시 부산진구 부전동",
      "expected": "부산광역시 부산진구 부전동" }
  ]
}
  • 케이스가 활성 zone을 zones 키 목록으로 참조한다 — 기존 테스트가 zone별 독립 구조(서면·tall·big·hot의 좌표가 서로 겹침)라 전역 단일 목록으로는 이식이 안 된다. 케이스별 참조면 좌표 재배치 없이 기존 7건을 1:1 이식한다.
  • 검증 의미는 하나로 통일: displayName(gridId, 활성 zones, regionName) == expected. 기존 contains boolean 검증(사각형 밖 3건)은 폴백 expected 케이스로 표현한다. expected: null = 표시명 없음(regionName도 null).
  • 픽스처 값은 기존 테스트 상수를 그대로 이식한다(m234 접두 포함) — "기존 7건 green 유지" 회귀 판정이 diff로 자명해진다.
  • 테스트는 Jackson으로 파일을 읽어 @TestFactory(또는 @ParameterizedTest) 동적 케이스로 돈다. 참조 구현(contains/label/pick/displayName 4개 static 메서드)은 테스트 안에 그대로 유지 — 이 테스트가 여전히 규칙의 실행형 정본이다.
  • FE 공유 경로 (레포 밖 참조 문제): 별도 계약 레포·아티팩트 배포는 만들지 않는다 — 파일 하나에 오버(FE도 팀 GitHub에서 raw 파일 접근 가능). 정본 = 레포 파일, 위키 zone 표시명 FE 계약 §3의 기대값 표는 파일 링크로 대체한다(노트가 이미 "파일이 나오면 그것이 정본"으로 예고해 둠).
  • 기각: (a) 위키 표만 유지 — 표는 사람이 손으로 베낀 사본이라 드리프트 원점. (b) 공유 계약 레포 신설 — 파일 1개에 레포·CI·버전 관리 오버헤드.

D-5 — 캐시 무효화 = 세션 1회 fetch로 충분, 버전/ETag 신설 안 함 (PRD §8 ②)

  • 결정: GET /api/zones앱 진입 시 1회 조회, 세션 동안 재사용, 재접속 = 최신이 공식 규칙이다(FR-15). 버전 필드·ETag·폴링을 만들지 않는다. 이 규칙을 위키 FE 계약 노트 §1의 "미해결로 관리 중" 문구를 "확정"으로 바꿔 명문화한다.
  • 근거: ① zone 데이터는 검수 확정 후 사실상 정적 — 갱신은 배포 이벤트 단위(연 몇 회). ② 낡음의 피해가 표시명 문자열 하나뿐이라 기능 깨짐이 없고, 세션 종료·재접속이면 자연 해소. ③ 응답이 40건·수 KB라 조건부 요청의 절약 실익도 없다.
  • 승격 조건: zone을 운영 중 상시 편집하는 도구가 생기면 그때 ETag(표준 HTTP, 신규 필드 불요)부터 검토.

D-6 — 초안 파일은 미커밋, 스크립트만 커밋 (PRD §8 ③)

  • 결정: scripts/zones-draft/스크립트 3종만 커밋한다(convert-zones.py·build-candidate.py·validate-zones.py). 산출물(zones-draft.json/.geojson 5.4천 줄, zones-candidate.json/.geojson)은 커밋하지 않고 전용 scripts/zones-draft/.gitignore(*.json·*.geojson)로 제외한다 — 루트 .gitignore를 건드리지 않아 규칙의 소속이 폴더에서 자명하다.
  • 근거: 정본은 seed/zones.json 하나다. 초안·후보는 전부 스크립트 재실행으로 재생성 가능한 파생물 — draft 재생성에 필요한 공공데이터 원천(소진공 주요상권현황 CSV·회식상권 SHP)의 출처·다운로드 방법은 위키 zone 표시명 데이터 파이프라인 해설(06-research, 게시 완료)에 기록돼 있다. 5.4천 줄 데이터 덤프는 레포 소음이다.

D-7 — zones.region_code 채움 = 이번 스코프 제외 (PRD §8 ④)

  • 결정: 확정본 전건 regionCode: null로 시딩한다. 중심점 point-in-polygon 배치는 만들지 않는다.
  • 근거: 소비처가 0이다 — nullable FK(문맥용)이고, FE 계약 노트도 "계산엔 불필요"로 명시하며, 표시명 산술·폴백 어느 경로도 이 값을 읽지 않는다. 채우는 작업(zone 중심점 → regions 공간 조인 배치)은 소비처가 생길 때 별도 티켓 — 그때 UPSERT 재시딩만으로 반영된다(스키마·시더 무변경).

D-8 — 시더 플래그 = prod/dev 프로파일 상시 on, 기본값 off 유지 (추가 결정 사항)

  • 결정: application-prod.yml·application-dev.ymlfillmap.zone.seed.enabled: true를 추가해 배포 환경에서는 기동마다 시딩이 돈다. 코드 기본값(false)은 유지 — 로컬·테스트는 지금처럼 무영향.
  • 근거: ① 멱등 UPSERT 40행이라 기동 비용·부작용이 무시 가능(zone_key 자연키, 재실행 수렴 — 기존 시더 그대로). ② 수동 플래그 토글은 잊히는 운영 스텝 — 상시 on이면 zones.json 수정이 배포만으로 반영된다. ③ RegionSeeder(기본 off·수동 1회) 선례와 다르지만, regions는 3,558행 + 대형 geometry라 기동 비용이 다르다 — 40행 정수 테이블에는 상시 on이 맞다.
  • 삭제 미동기화 원칙(명문화): UPSERT는 행을 지우지 못한다 — zones.json에서 zone을 빼도 DB 행은 남는다. zone 제거는 해당 zone_key 행 수동 DELETE가 공식 절차다(PRD 비기능 "롤백" 조항과 동일). zone_key 변경도 사실상 삭제+추가라 같은 절차를 따른다.
  • 주의: 이 yml 1줄은 설정이지 Java가 아니다 — "프로덕션 Java 변경 0" 원칙과 충돌하지 않는다.

D-9 — glossary 등재 초안 (FR-16 — 아래 텍스트를 ✅ MVP 확정 용어 섹션에 추가)

### 구역 (Zone)

**정의**: "서면"·"홍대입구"처럼 행정동으로 표현되지 않는 유명 통칭을 나타내는, 수동 지정된 격자 사각형.

- DB 테이블: `zones` — grid_y/grid_x 정수 범위(min/max) 사각형, PostGIS 불필요 (MSG-234 V8)
- `zone_key`(안정 slug, 예: `seomyeon`)가 자연키 — 시딩 멱등 UPSERT·클라이언트 참조 기준. `name`은 사람용(비유일)
- 남북 26행(약 2.6km) 한계 — 표시명 행이 A~Z라 DB CHECK로 강제
- 데이터 출처: 소상공인시장진흥공단 공공 상권 경계 자동 변환 + 팀 검수 (MSG-259)
- **서로 겹치지 않게 관리**한다. `priority`는 실수 겹침 시 표시 결정성 보험(전원 0 — 서열 기준 없음)

### 표시명 (Display Name)

**정의**: 격자의 사람용 이름. 상태(점령)가 아니라 **이름**이다 — 점령/방문/도감과 혼용 금지.

- 격자가 어느 구역(zone) 안이면: `"{구역명} {행}-{열}"` (행 A = 사각형 북단, 열 1 = 서단) — 예: `"서면 A-14"`
- 구역 밖이면: 행정동 이름 폴백(`regionName`, 번호 없음). 행정동도 없으면(해안 등) 표시명 없음
- **클라이언트가 계산**한다(FE-local, MSG-234 §D3) — 서버 응답에 `displayName` 필드는 없다
- 규칙의 실행형 정본: `src/test/resources/fixtures/zone-naming.json` (언어 중립 픽스처, MSG-259)

D-10 — 위키 정리 = 잔여 2건만 (FR-17 실사 결과 축소)

  • 결정: 실사 결과 ADR(ADR 격자 표시명 zone)의 상태 정정 배너(2026-07-30)와 06-research 파이프라인 해설 노트는 이미 게시돼 있다 — PRD §7의 위키 신규/정정 작업 중 잔여는 2건뿐:
  • zone 표시명 FE 계약 §3 기대값 표 → 픽스처 파일 정본 링크로 갱신(D-4), §1 캐시 문구 → 확정 표기(D-5).
  • ADR "MSG-259 진행 중" → 시딩 완료 시 종결 표기 (Wrap-up).

API 명세

변경 없음. 출하 엔드포인트·DTO는 MSG-234 그대로 — GET /api/zones(인증 필수, List<ZoneResponseDto>, 시딩 전 빈 배열). 이번 티켓으로 달라지는 것은 응답 내용물뿐이다: [] → 확정본 30~50건. 신규 에러 코드 없음.

도메인 로직

변경 없음. 명명 산술(행 = maxGridY − gridY → A~Z, 열 = gridX − minGridX + 1, 폴백 = 행정동 이름)의 정본은 MSG-234 §D2·§도메인 로직이고, 실행형 정본이 Java 테스트에서 JSON 픽스처로 승격되는 것(D-4)이 이번 변경의 전부다. 겹침 결정성(priority DESC, zoneKey ASC)은 D-2에 따라 데이터가 발동시키지 않는 보험으로 유지.

데이터 모델

  • Flyway 마이그레이션 없음 — V8 배포 완료, 스키마·엔티티·시더 무변경.
  • src/main/resources/seed/zones.json: [] → 확정본 30~50건. 항목 형식은 기존 ZoneSeed 그대로 ({zoneKey, name, regionCode, minGridY, maxGridY, minGridX, maxGridX, priority}) — regionCode 전건 null(D-7), priority 전건 0(D-2).
  • 시딩 실행: prod/dev 상시 on(D-8), 멱등 UPSERT(ON CONFLICT (zone_key) DO UPDATE), 삭제는 수동 DELETE.

계약 변경

없음. 크로스오너 계약 4종(GridQueryService·HotZoneService·UserGridQueryService·UserOidcCommandService) 시그니처 불변, Owner B 코드 변경 0, GET /api/zones 응답 형식 불변. 신설되는 픽스처 파일은 FE/모바일이 참조하는 검증 자료이지 서버 런타임 계약면이 아니다.

변경 파일 목록

파일 변경 근거
src/main/resources/seed/zones.json [] → 확정본 30~50건 FR-1~7·D-1·D-2·D-7
src/test/resources/fixtures/zone-naming.json 신규 — 언어 중립 명명 계약 픽스처 FR-14·D-4
src/test/java/com/msg/fillmap/zone/ZoneNamingContractTest.java 픽스처 파일 로드로 리팩터 (시나리오 7건 유지) FR-14·D-4
src/main/resources/application-prod.yml · application-dev.yml fillmap.zone.seed.enabled: true 1줄 D-8
scripts/zones-draft/{convert-zones,build-candidate,validate-zones}.py 커밋 (본 브랜치 chore 커밋) FR-7a·D-6
scripts/zones-draft/.gitignore 신규 — 산출물(json/geojson) 제외 D-6
.claude/rules/glossary.md "구역(zone)"·"표시명(display name)" 등재 FR-16·D-9
.claude/docs/status.md zone 데이터 주입 반영 (한 줄 append) Wrap-up
../LLM-WIKI/03-specs/zone 표시명 FE 계약.md 픽스처 정본 링크·캐시 규칙 확정 표기 D-4·D-5·D-10
../LLM-WIKI/04-decisions/ADR 격자 표시명 zone.md 시딩 완료 시 종결 표기 D-10

변경 없음: zone 패키지 프로덕션 Java 전체(Zone·ZoneRepository·ZoneQueryService·ZoneController·ZoneResponseDto·ZoneSeeder) · DB 마이그레이션 · Owner B 코드 · /api/grids 뷰포트 경로.


테스트 시나리오 (모듈 단위)

핵심 회귀 기준: 픽스처 리팩터 후 ZoneNamingContractTest 기존 7건 시나리오 전부 green — 값·시나리오를 바꾸지 않고 소스만 하드코딩 상수 → JSON 파일로 옮긴다.

모듈 1 — 명명 계약 픽스처 리팩터 (ZoneNamingContractTest, 순수 함수·DB 불요)

기존 7건이 픽스처 케이스로 1:1 이식되어 green 유지 (케이스 이름·입력·기대값 동일): - 북단_서단_격자는_A_1로_명명된다 - 북단에서_14번째_열은_A_14다 - 한_행_남쪽은_B로_내려간다 - 남단_행은_사각형_높이에_따라_알파벳이_증가한다 (Z 경계) - 사각형_밖_격자는_zone_매칭이_아니다 (폴백 expected 케이스로 표현 — D-4) - 두_zone에_겹치는_격자는_priority가_높은_zone을_고른다 - 매칭_zone이_없으면_행정동_이름으로_폴백한다

추가 검증 1건: - 픽스처_케이스가_비어있으면_테스트가_실패한다 (파일 누락·파싱 실패가 조용히 0건 통과로 새지 않게 — 케이스 수 > 0 가드)

모듈 2 — 시딩 데이터 검증 (Java 테스트 아님 — D-3 3층)

  • validate-zones.py seed/zones.jsonPASS (26행 캡·겹침·키중복·역전 0) — 시딩 전 필수 게이트
  • 시딩 시 V8 CHECK 통과 (전건 적재 — 위반 0이 이미 1층에서 보증됨)
  • geojson.io 오버레이 육안 검수 (위치·이름 — 특히 갭필 2건 광안리·해운대, FR-4)

모듈 3 — 전체 회귀 (프로덕션 무변경 확인)

  • ./gradlew test 전체 green — zone 기존 테스트 23건 포함, 프로덕션 Java diff 0 확인
  • 기존 ZoneSeeder 테스트 4건(플래그 게이트·멱등·갱신 수렴)이 그대로 green — 시더 무변경의 방증

DoD 수동 검증 (로컬)

  • 로컬에서 fillmap.zone.seed.enabled=true 기동 → GET /api/zones에 확정본 전건(30~50) 등장
  • 서면 사각형 북단·서단 격자 gridId로 FE 산술 재현 = "서면 A-1", 드라이런 조정 열(서면 maxGridX=112229) 바로 동쪽 격자는 전포로 매칭
  • 시더 2회 기동 → 행 수 불변 (멱등 재확인)

미해결 질문

  1. 유명 통칭 최종 목록build-candidate.pyFAMOUS 23개 + 수동 작도 대상(missing 목록)은 제안이다. FR-1의 "임의 선정"은 팀 판단 — 스크럼에서 목록을 확정해야 확정본 건수(30~50 범위)가 정해진다. 확정 즉시 FAMOUS·ADJUSTMENTS 갱신 → 재실행 → validate PASS → 시딩.
  2. 수동 작도 13곳의 bbox 실값 — 공공데이터에 없어 지도 대조로 사람이 정하는 값(가로수길·연남·대학로·샤로수길·광복·동래·경성대부경대 등). 검수 세션 산출물이라 스펙이 미리 못 박을 수 없다 — 작도 후 validate·geojson.io 게이트(D-3)는 동일 적용.

결정으로 종결(비-Open): 검수 4단계(D-1)·priority 전원 0(D-2)·검증 3층(D-3)·픽스처 형식/위치(D-4)·캐시 무효화 불요(D-5)·초안 미커밋(D-6)·region_code 유예(D-7)·시더 상시 on + 삭제 수동(D-8)·glossary 초안(D-9)·위키 잔여 2건(D-10).


Wrap-up 체크리스트 (구현 후)

  • [ ] 커밋 분리(작업 단위): MSG-259 chore: zones 검수 파이프라인 스크립트 3종 + gitignore / MSG-259 feat: 상권 확정본 zones.json 시딩 데이터 + prod/dev 시더 상시 on / MSG-259 test: 명명 계약 언어중립 JSON 픽스처 추출 / MSG-259 docs: glossary 구역·표시명 등재 + status.md
  • [ ] glossary 등재(D-9 텍스트), status.md 한 줄 append
  • [ ] 위키: FE 계약 노트 픽스처 링크·캐시 확정(D-10-1), ADR 종결 표기(D-10-2)
  • [ ] 지라 MSG-259에 D-결정·Open Q 요약 코멘트
  • [ ] PR 본문은 .github/PULL_REQUEST_TEMPLATE.md 구조, 변경 파일 실측(git diff --stat) 포함