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) — V8zones테이블·GET /api/zones·ZoneSeeder·테스트 23건. 기계장치는 전부 있고 데이터가 0행이다(seed/zones.json=[]). 상세 결정 이력:docs/spec/MSG-234.md§D1~D8. 드라이런 완료(2026-07-30):scripts/zones-draft/build-candidate.py→zones-candidate.json17건,validate-zones.pyPASS. 경계 조정 실값: 서면maxGridX112230→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 소관).
성공 기준 (관찰 가능)¶
seed/zones.json이 확정본 30~50건이고,scripts/zones-draft/validate-zones.py가 PASS한다(26행 캡·겹침·키중복·범위역전 0 — FR-6·FR-7·FR-7a).- 시딩 후
GET /api/zones에 확정본 전건이 등장하고, 서면 사각형 북단·서단 격자의 FE 산술 재현값이"서면 A-1"이다(FR-9). 재시딩해도 결과가 같다(FR-8, 기존 시더 그대로). ZoneNamingContractTest가src/test/resources/fixtures/zone-naming.json에서 케이스를 읽으며, 기존 7건 시나리오가 전부 green을 유지한다(핵심 회귀 기준, FR-14).- 픽스처 파일은 Java 문법이 하나도 없는 순수 JSON이라 FE/모바일이 그대로 파싱해 검증할 수 있고, 위키 zone 표시명 FE 계약 노트가 이 파일을 정본으로 가리킨다.
- glossary에 "구역(zone)"·"표시명(display name)" 정의가 있다(FR-16, §D-9 초안 그대로).
- zone 패키지 프로덕션 Java 변경 0 · 크로스오너 계약 4종 시그니처 불변 · 전체 테스트 green.
- 시딩 전(0행) 동작은 지금과 동일 — 전 화면 행정동 폴백, 에러 없음(FR-12, 기존 테스트가 이미 보증).
결정 (Decisions)¶
D-1 — 검수 파이프라인 = 4단계 (지라 확정 2026-07-30 승격)¶
- 결정: 확정본은 다음 4단계로 만든다.
- 유명 통칭 목록 임의 선정(30~50개) — 우체국·지구대류 비상권 이름과 안 유명한 이웃 상권(역삼·합정 등)은 목록에서 빼서 겹침을 원천 차단(FR-1). 제안 목록은
build-candidate.py의FAMOUS딕셔너리(23개), 팀 스크럼에서 확정. - 같은 시/도 안에서만 합집합 bbox — "서면역 7번/8번/13번 출구" → "서면" 1건. 시/도가 다르면 동명이라도 합치지 않는다(서울시청+부산시청 = 265km 사각형 방지, FR-2).
- 공공데이터에 없는 유명지는 수동 작도 — 상가 밀집 기준 데이터라 통칭 유명세와 어긋나는 구멍(가로수길·연남·광복 등, FR-2a). 목록 확정 시 변동 —
build-candidate.py가 출력하는 missing 목록이 작업 리스트. - 겹침은 침범한 쪽 min/max 정수 조정으로 분리 — 드라이런 실측 2쌍(서면↔전포·홍대입구↔신촌, 각각 한 열 침범). 조정 실값은
build-candidate.py의ADJUSTMENTS에 코드로 고정(서면maxGridX112229, 홍대입구 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.pyPASS 필수(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. 기존containsboolean 검증(사각형 밖 3건)은 폴백 expected 케이스로 표현한다.expected: null= 표시명 없음(regionName도 null). - 픽스처 값은 기존 테스트 상수를 그대로 이식한다(m234 접두 포함) — "기존 7건 green 유지" 회귀 판정이 diff로 자명해진다.
- 테스트는 Jackson으로 파일을 읽어
@TestFactory(또는@ParameterizedTest) 동적 케이스로 돈다. 참조 구현(contains/label/pick/displayName4개 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/.geojson5.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.yml에fillmap.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.json→ PASS (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회 기동 → 행 수 불변 (멱등 재확인)
미해결 질문¶
- 유명 통칭 최종 목록 —
build-candidate.py의FAMOUS23개 + 수동 작도 대상(missing 목록)은 제안이다. FR-1의 "임의 선정"은 팀 판단 — 스크럼에서 목록을 확정해야 확정본 건수(30~50 범위)가 정해진다. 확정 즉시FAMOUS·ADJUSTMENTS갱신 → 재실행 → validate PASS → 시딩. - 수동 작도 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) 포함