# 설계서: Restaurant 임의 필드 업데이트 화이트리스트 (#332)

> **상태**: Approved
> **작성**: [AI] Architect · **최종수정**: 2026-06-15
> **추적성** — Redmine: #332 · 부모: #290 (식당 CRUD Reviewer 후속, 09-Done)
> · 구현 파일: `backend-java/src/main/java/com/tasteby/controller/RestaurantController.java`, `backend-java/src/main/java/com/tasteby/service/RestaurantService.java`
> · 테스트: 수동 (curl로 화이트리스트 외 필드 시도)

## 1. 목적 (Why)

`PUT /api/restaurants/{id}` body가 `Map<String, Object>`로 임의 키를 받는다. SQL 측 `updateFields`는 컬럼별 `<if test="containsKey('...')">` 가드로 화이트리스트가 이미 적용되어 있어 임의 컬럼 갱신은 차단된다. 다만 Controller 레벨에서 화이트리스트가 명시되지 않아 — (a) 의도 모호, (b) 향후 SQL 측 가드가 무력화되거나 다른 매퍼로 확장되면 위험. 명시적 화이트리스트로 의도 강화.

## 2. 범위 (Scope)

- **포함**
  - `RestaurantController.update(id, body)`에서 허용된 키만 통과시키는 `ALLOWED_UPDATE_FIELDS` set.
  - 허용되지 않은 키는 무시(silently drop) + 디버그 로그. 400 응답은 hard policy라 사용자 경험 영향 큼.
- **제외 (out of scope)**
  - DTO 클래스 도입 (RestaurantUpdateDTO + Bean Validation) — 더 강한 표준화지만 코드 영향 큼. 후속.
  - DDG HTML 검색 → 정식 API 전환 (별도 후속, 비용/계약 결정 필요).
  - isNameSimilar 한국어 자모 알고리즘 (별도 후속).
  - UNIQUE(google_place_id) 제약 강화 (DB 마이그레이션 + 데이터 정리 필요, 별도).

## 3. 인수조건

- [ ] `ALLOWED_UPDATE_FIELDS` 상수 정의 (SQL `updateFields` 가드와 일치).
- [ ] PUT 호출 시 body에서 허용 외 키는 자동 제거 + 디버그 로그.
- [ ] 허용 키만 있는 정상 호출 → 200 정상 동작 회귀 없음.
- [ ] 허용 외 키만 있는 호출 → 200 + 변경 없음 (또는 200 + 빈 업데이트).

## 4. 컨텍스트 & 제약

- Spring MVC. 변경 최소화. DTO 도입 없이도 가드 가능.
- 운영 영향 없음 (어드민이 화면에서 호출하는 키는 모두 허용 set 안에).
- 가정: 화이트리스트 set은 SQL `updateFields` 의 `<if>` 키들과 1:1.

## 5. 아키텍처 개요

```
PUT /api/restaurants/{id}  body
        │
        ▼
RestaurantController.update
   ├ requireAdmin
   ├ allowed = ALLOWED_UPDATE_FIELDS
   ├ filtered = body where key ∈ allowed
   ├ if (filtered.isEmpty()) → 200 + no-op
   └ restaurantService.update(id, filtered) → mapper.updateFields
```

## 6. 데이터 모델

`Set<String> ALLOWED_UPDATE_FIELDS = Set.of(name, address, region, cuisine_type, price_range, phone, website, tabling_url, catchtable_url, latitude, longitude, google_place_id, business_status, rating, rating_count)` — SQL `updateFields`의 키들과 일치.

## 7. 함수 명세

| 함수 | 책임 | 비고 |
|------|------|------|
| `RestaurantController.update(id, body)` (수정) | 화이트리스트 필터 후 위임 | filtered.isEmpty()면 no-op |

## 8. 흐름

1. body Map 수신.
2. `body.entrySet().stream().filter(e -> ALLOWED_UPDATE_FIELDS.contains(e.getKey())).collect(toMap)`.
3. 비어있으면 200 응답하고 끝.
4. 아니면 `restaurantService.update(id, filtered)` 호출.

## 9. 엣지케이스

- **빈 body**: 200 + no-op.
- **허용 외 키만**: 200 + no-op + 디버그 로그.
- **null 값을 포함한 허용 키**: SQL `updateFields`가 그대로 NULL 저장 — 의도된 동작 (좌표/주소 해제 등).

## 10. 테스트

- 수동:
  ```
  curl -X PUT -H "Authorization: Bearer <admin>" -H "Content-Type: application/json" \
       -d '{"name":"테스트", "is_admin":true}' /api/restaurants/<id>
  → name만 갱신, is_admin은 무시
  ```
- 자동: 별도 후속(통합 테스트 인프라 도입 시).

## 11. 리스크 & 대안

- **선택**: Controller 화이트리스트 set + silent drop.
- **대안 A**: DTO + Bean Validation — 표준화 깔끔하지만 변경 범위 큼.
- **대안 B**: 허용 외 키 발견 시 400 — 사용자 경험 부담, 클라이언트가 잘못된 키 보내면 즉시 실패. 본 변경은 보수적으로 silent drop.

## 12. 미해결 질문

- DDG → 정식 검색 API 전환 — 별도 후속 (비용/계약 결정).
- isNameSimilar 한국어 알고리즘 — 별도 후속.
- DTO 도입 표준화 — 별도 후속.
