지역 DB 스키마
Status: Implemented
목적
이 문서는 ZIP:ON 지역 조회 기능이 사용하는 regions 테이블과 MyBatis mapper를 설명한다. 여기서 말하는 Region API는 법정동/지역 기준 데이터를 조회하는 legacy 보조 API이며, 사용자가 강남 원룸처럼 입력했을 때 실행되는 지역·유형 과거 지표 분석은 RegionalIndicatorAnalysisController와 RegionalIndicatorAnalysisService가 담당한다.
현재 지역 schema의 source of truth는 backend/src/main/resources/db/migration/V2__create_region_schema.sql이다. 애플리케이션 데이터 접근은 backend/src/main/java/com/zipon/mapper/RegionMapper.java가 담당한다.
관련 문서:
현재 구현 범위
이번 단계에서 실제 DB와 연결된 API:
GET /api/regions
GET /api/regions/{regionId}
GET /api/regions/{regionId}/summary현재 legacy 빈 응답 계약으로 남긴 API:
GET /api/regions/{regionId}/trendGET /api/regions/{regionId}/summary는 현재 RegionService.getRegionDetail(...)과 같은 기본 지역 정보를 반환한다. GET /api/regions/{regionId}/trend는 응답 shape가 확정되지 않아 빈 Map을 반환한다. real_estate_transaction_facts, market_statistics_monthly, market_indicator_trend_summaries 같은 저장 schema는 생겼지만, 이 legacy region summary/trend endpoint에는 아직 연결하지 않았다. 평균 매매가, 평균 전세가, 거래량, 가격 변화율은 새 Region 상세 요구가 생기면 RegionalIndicatorAnalysisService의 지역·유형 과거 지표 분석과 중복되지 않게 별도 slice로 설계한다.
현재 프론트엔드 연결 상태:
frontend/src/api/regionApi.js에는getRegionList,getRegionDetail,getRegionSummary,getRegionTrend함수가 있다.frontend/src/views/RegionDetailView.vue는 아직 이 함수들을 호출하지 않고, 현재 매물 목록이 아니라 지역·유형 과거 지표 기준과 정확 주소 진단 연결을 안내하는 정적 화면이다.- 지역·유형 과거 지표 화면은
frontend/src/views/SearchResultView.vue가frontend/src/api/regionalIndicatorAnalysisApi.js의createRegionalIndicatorAnalysis(payload)를 호출하는 별도 흐름이다.
regions
Domain object: backend/src/main/java/com/zipon/domain/Region.java
Mapper: backend/src/main/java/com/zipon/mapper/RegionMapper.java
Migration: backend/src/main/resources/db/migration/V2__create_region_schema.sql
Service owner: backend/src/main/java/com/zipon/service/RegionService.java
Test: backend/src/test/java/com/zipon/RegionIntegrationTest.java
목적:
- 시/도, 시/군/구, 동 같은 지역 단위를 저장한다.
- 매물, 실거래, 지도, 검색 기능이 참조할 기준 데이터를 제공한다.
- 지역명 검색과 법정동 코드 기반 조회의 시작점이 된다.
주요 컬럼:
id PK, auto increment
region_name 화면 표시와 지역명 검색에 쓰는 이름
legal_code 법정동 코드 또는 행정구역 코드 후보, unique
parent_region_id 상위 지역을 가리키는 self foreign key
created_at row 생성 시각
updated_at row 수정 시각 후보제약과 인덱스:
uk_regions_legal_code: 같은 법정동 코드가 중복 저장되지 않게 막는다.fk_regions_parent_region:regions.parent_region_id가 같은 테이블의regions.id를 참조한다.idx_regions_region_name: 지역명 검색 후보 경로를 지원한다.idx_regions_parent_region_id: 하위 지역 조회 후보 경로를 지원한다.
Nullable 결정:
region_name: not null. 화면 표시와 검색의 기본값이므로 비어 있으면 안 된다.legal_code: not null. 외부 공공데이터와 연결할 기준 식별자로 사용한다.parent_region_id: nullable. 최상위 지역은 부모가 없을 수 있다.created_at,updated_at: not null. migration에서 기본값을 둔다.
요청 흐름
지역 목록 조회:
GET /api/regions?regionName=강남
-> RegionController.getRegionList(...)
-> RegionService.getRegionList(...)
-> RegionMapper.findBySearchCondition(regionName, legalCode, limit)
-> regions table
-> RegionSummaryResponse지역 요약 조회:
GET /api/regions/{regionId}/summary
-> RegionController.getRegionSummary(...)
-> RegionService.getRegionSummary(...)
-> RegionService.getRegionDetail(...)
-> 현재는 RegionSummaryResponse의 통계 필드 null지역 가격 추이 조회:
GET /api/regions/{regionId}/trend
-> RegionController.getRegionTrend(...)
-> RegionService.getRegionTrend(...)
-> 현재는 차트 DTO 미확정으로 빈 Map 반환지역 상세 조회:
GET /api/regions/{regionId}
-> RegionController.getRegionDetail(...)
-> RegionService.getRegionDetail(...)
-> RegionMapper.findById(regionId)
-> 없으면 NotFoundException
-> GlobalExceptionHandler.handleNotFoundException(...)
-> 404 ErrorResponseMapper SQL 기준
RegionMapper.findBySearchCondition(...)는 두 조건을 받는다.
regionName: 부분 검색
legalCode: 정확히 일치
limit: 기본 20건현재 SQL은 regions를 자기 자신과 LEFT JOIN해서 parent_region_name도 함께 읽는다. 지금 RegionSummaryResponse에는 부모 지역명을 직접 노출하지 않지만, domain object에는 parentRegionId, parentRegionName을 보존한다. 이후 지역 breadcrumb, 자동완성 설명, 지도 검색 결과 설명에 사용할 수 있다.
RegionMapper는 market_statistics_monthly나 market_indicator_trend_summaries를 읽지 않는다. 그 데이터는 현재 RegionalIndicatorAnalysisService -> MarketIndicatorContextService -> RegionalIndicatorAnalysisMapper 경계에서 읽는다. 이렇게 나누는 이유는 regions 기준 데이터 조회와 사용자 입력 기반 지표 해석의 책임이 다르기 때문이다.
Service 책임
RegionService는 다음 책임을 가진다.
- 빈 문자열 검색 조건을
null로 정규화한다. - mapper 결과인
Regiondomain object를RegionSummaryResponse로 변환한다. - 존재하지 않는
regionId를NotFoundException으로 바꾼다. - 아직 이 endpoint에 연결하지 않은 가격 통계 필드는
null로 둔다. getRegionSummary(...)는 현재getRegionDetail(...)을 재사용한다.getRegionTrend(...)는 차트 응답 DTO를 정하기 전까지 빈Map을 반환한다.
RegionService가 직접 SQL을 만들지 않는 이유:
- SQL은
RegionMapper가 가진다. - Service는 요청 조건 해석, 예외 판단, DTO 변환을 담당한다.
- 이렇게 나누면 schema와 SQL을 바꿔도 Controller의 HTTP 계약은 크게 흔들리지 않는다.
테스트
테스트 파일:
backend/src/test/java/com/zipon/RegionIntegrationTest.java검증하는 동작:
- Flyway가
V1__create_auth_schema.sql과V2__create_region_schema.sql을 순서대로 적용한다. GET /api/regions?regionName=강남이 지역명 부분 검색 결과를 반환한다.GET /api/regions?legalCode=1171000000이 법정동 코드 정확 일치 결과를 반환한다.GET /api/regions/{regionId}가 존재하는 지역을 반환한다.- 없는 지역 ID는 404와
"존재하지 않는 지역입니다."메시지를 반환한다.
현재 RegionIntegrationTest는 summary와 trend endpoint를 직접 검증하지 않는다. 이 두 endpoint에 실제 통계 계산을 붙이는 순간에는 별도 테스트를 추가해야 한다. 특히 trend는 Map<String, Object> 대신 전용 response DTO로 바꾸는 것이 먼저다.
실행:
cd backend
./mvnw -Dtest=RegionIntegrationTest testDebugging checklist
지역 조회가 실패하면 아래 순서로 확인한다.
1. flyway_schema_history에 V2__create_region_schema.sql이 적용됐는가?
2. regions table에 테스트 또는 로컬 데이터가 들어 있는가?
3. RegionMapper SQL의 column alias가 Region 필드명과 맞는가?
4. mybatis.configuration.map-underscore-to-camel-case=true가 유지되는가?
5. legal_code unique constraint 때문에 중복 insert가 실패한 것은 아닌가?
6. parent_region_id가 존재하지 않는 regions.id를 참조하지 않는가?
7. GET /api/regions/** 경로가 SecurityConfig에서 permitAll인지 확인했는가?
8. 없는 ID가 500이 아니라 404로 내려가는가?Design tradeoffs
regions는 자기참조 FK를 사용한다. parent_region_name 문자열만 저장하면 조회는 단순하지만, 같은 이름의 지역이 생기거나 상위 지역명이 바뀔 때 데이터 정합성이 약해진다. 그래서 schema에는 parent_region_id를 두고, mapper에서 필요한 경우 부모 이름을 join해서 읽는다.
아직 RegionDetailResponse를 따로 만들지 않았다. 현재 화면과 API 지도는 RegionSummaryResponse를 사용하고 있고, 가격 통계가 아직 없기 때문이다. 상세 화면에 부모 지역, 하위 지역 목록, 지도 중심 좌표, 통계 기준일 같은 필드가 필요해지면 RegionDetailResponse를 분리한다.
RegionService.getRegionSummary(...)에 곧바로 R-ONE/실거래가 분석을 붙이지 않은 것은 의도적인 경계다. 지역·유형 입력은 RegionalIndicatorAnalysisService.analyze(...)가 locationKeyword, typeHints, contractPurpose, precisionLevel을 함께 해석해야 하고, 단순 regionId 조회와 필요한 문장/한계/다음 행동이 다르다.
Learning path
- First read:
backend/src/main/resources/db/migration/V2__create_region_schema.sql - Then inspect:
backend/src/main/java/com/zipon/mapper/RegionMapper.java - Then inspect:
backend/src/main/java/com/zipon/service/RegionService.java - Then run:
cd backend && ./mvnw -Dtest=RegionIntegrationTest test - Then debug:
flyway_schema_history,regions,RegionMapper.findBySearchCondition(...) - Then compare:
backend/src/main/java/com/zipon/service/RegionalIndicatorAnalysisService.java - Key concept to understand: Flyway migration이 table 구조를 만들고, MyBatis mapper SQL이 조회 방식을 정하며, Service가 HTTP 응답에 맞게 domain object를 DTO로 바꾼다. Region API는 기준 데이터 조회이고, Regional Indicator Analysis API는 사용자 입력을 과거 지표 리포트로 해석하는 use case다.