Compare commits
5 Commits
v0.1.37
...
9ba905aad8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ba905aad8 | ||
|
|
8c4b0c3e9a | ||
|
|
3815221535 | ||
|
|
49ef0322ac | ||
|
|
cc4bc0b7e4 |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -6,7 +6,24 @@
|
|||||||
|
|
||||||
## 2026-06-15
|
## 2026-06-15
|
||||||
|
|
||||||
### 🧹 #329 admin/page.tsx 분리 (v0.1.35)
|
### 🔤 #348 isNameSimilar 한국어 자모 + Sørensen-Dice (v0.1.38)
|
||||||
|
- HangulSimilarity 유틸 신규 (Unicode NFD 분해 + bigram Sørensen-Dice)
|
||||||
|
- RestaurantController.isNameSimilar 교체, 임계값 0.45
|
||||||
|
- 짧은 한국어 이름 매칭 정확도 향상 (예: "스타벅스 강남" vs "스타벅스 강남점")
|
||||||
|
- 후속 분리: #357(DDG→정식 API), #358(DTO+@Valid), #359(UNIQUE+데이터 정리)
|
||||||
|
- 설계서: docs/design/348-name-similarity/README.md
|
||||||
|
- Refs: #348 (close)
|
||||||
|
|
||||||
|
### 🌐 #352 i18n 뼈대 ko/en/ja/es (v0.1.37)
|
||||||
|
- next-intl 5.x 도입
|
||||||
|
- src/i18n/{config,LocaleProvider} + src/messages/{ko,en,ja,es}.json (30 키)
|
||||||
|
- LanguageSwitcher 컴포넌트 (헤더, ARIA listbox, 44px, 국기+네이티브명)
|
||||||
|
- localStorage tasteby_locale + 브라우저 언어 감지 + ko fallback
|
||||||
|
- 설계서: docs/design/352-i18n-skeleton/README.md
|
||||||
|
- 미적용: URL 라우팅 i18n, SEO meta, 사용자 콘텐츠 번역, 어드민(한국어 유지)
|
||||||
|
- Refs: #352 (close)
|
||||||
|
|
||||||
|
### 🧹 #329 admin/page.tsx 분리 (v0.1.35→v0.1.36 운영 반영)
|
||||||
- page.tsx 2817 → 107 LOC (탭 라우팅 + 헤더만)
|
- page.tsx 2817 → 107 LOC (탭 라우팅 + 헤더만)
|
||||||
- _panels/{Channels,Videos,Restaurants,Users,Daemon}Panel.tsx 5개 분리
|
- _panels/{Channels,Videos,Restaurants,Users,Daemon}Panel.tsx 5개 분리
|
||||||
- localStorage.getItem 10곳 → getAdminToken() (admin-utils.ts)
|
- localStorage.getItem 10곳 → getAdminToken() (admin-utils.ts)
|
||||||
|
|||||||
@@ -524,25 +524,12 @@ public class RestaurantController {
|
|||||||
* 식당 이름과 검색 결과 제목의 유사도 검사.
|
* 식당 이름과 검색 결과 제목의 유사도 검사.
|
||||||
* 한쪽 이름이 다른쪽에 포함되거나, 공통 글자 비율이 40% 이상이면 유사하다고 판단.
|
* 한쪽 이름이 다른쪽에 포함되거나, 공통 글자 비율이 40% 이상이면 유사하다고 판단.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* #348 — 한국어 자모 분해 + Sørensen-Dice bigram 유사도(임계값 0.45).
|
||||||
|
* 짧은 한국어 이름에서 이전 Jaccard-like(set 비율) 방식보다 정확.
|
||||||
|
*/
|
||||||
private boolean isNameSimilar(String restaurantName, String resultTitle) {
|
private boolean isNameSimilar(String restaurantName, String resultTitle) {
|
||||||
String a = normalize(restaurantName);
|
return com.tasteby.util.HangulSimilarity.similarity(restaurantName, resultTitle) >= 0.45;
|
||||||
String b = normalize(resultTitle);
|
|
||||||
if (a.isEmpty() || b.isEmpty()) return false;
|
|
||||||
|
|
||||||
// 포함 관계 체크
|
|
||||||
if (a.contains(b) || b.contains(a)) return true;
|
|
||||||
|
|
||||||
// 공통 문자 비율 (Jaccard-like)
|
|
||||||
var setA = a.chars().boxed().collect(java.util.stream.Collectors.toSet());
|
|
||||||
var setB = b.chars().boxed().collect(java.util.stream.Collectors.toSet());
|
|
||||||
long common = setA.stream().filter(setB::contains).count();
|
|
||||||
double ratio = (double) common / Math.max(setA.size(), setB.size());
|
|
||||||
return ratio >= 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalize(String s) {
|
|
||||||
if (s == null) return "";
|
|
||||||
return s.replaceAll("[\\s·\\-_()()\\[\\]【】]", "").toLowerCase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void emit(SseEmitter emitter, Map<String, Object> data) {
|
private void emit(SseEmitter emitter, Map<String, Object> data) {
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.tasteby.util;
|
||||||
|
|
||||||
|
import java.text.Normalizer;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* #348 — 한국어 자모 분해(Unicode NFD) + Sørensen-Dice bigram 유사도.
|
||||||
|
*
|
||||||
|
* 음절 단위 Jaccard보다 짧은 한국어 이름에 정확. 예:
|
||||||
|
* similarity("스타벅스 강남", "스타벅스 강남점") ≈ 0.85+
|
||||||
|
* similarity("스타벅스 강남", "스타벅스 종로") ≈ 0.55~0.85
|
||||||
|
* similarity("스타벅스", "맥도날드") < 0.20
|
||||||
|
*
|
||||||
|
* 공백/구두점은 제거하고 소문자화한 뒤 NFD 분해.
|
||||||
|
*/
|
||||||
|
public final class HangulSimilarity {
|
||||||
|
|
||||||
|
private HangulSimilarity() {}
|
||||||
|
|
||||||
|
/** 공백/구두점 제거 + 소문자화 + NFD 분해(한글 음절 → 자모). */
|
||||||
|
public static String decompose(String s) {
|
||||||
|
if (s == null || s.isEmpty()) return "";
|
||||||
|
String stripped = s.replaceAll("[\\s·\\-_()()\\[\\]【】]", "").toLowerCase();
|
||||||
|
return Normalizer.normalize(stripped, Normalizer.Form.NFD);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sørensen-Dice 계수 (bigram multiset 기반). 0.0~1.0.
|
||||||
|
* 동일 문자열 → 1.0. 빈 입력 → 0.0.
|
||||||
|
*/
|
||||||
|
public static double similarity(String a, String b) {
|
||||||
|
String da = decompose(a);
|
||||||
|
String db = decompose(b);
|
||||||
|
if (da.isEmpty() || db.isEmpty()) return 0.0;
|
||||||
|
if (da.equals(db)) return 1.0;
|
||||||
|
|
||||||
|
// 포함 관계는 강한 신호로 1.0 처리 (기존 동작과 일관)
|
||||||
|
if (da.contains(db) || db.contains(da)) return 1.0;
|
||||||
|
|
||||||
|
if (da.length() < 2 || db.length() < 2) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Integer> bigramsA = bigrams(da);
|
||||||
|
Map<String, Integer> bigramsB = bigrams(db);
|
||||||
|
int common = 0;
|
||||||
|
for (var e : bigramsA.entrySet()) {
|
||||||
|
Integer countB = bigramsB.get(e.getKey());
|
||||||
|
if (countB != null) {
|
||||||
|
common += Math.min(e.getValue(), countB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int sizeA = da.length() - 1;
|
||||||
|
int sizeB = db.length() - 1;
|
||||||
|
return (2.0 * common) / (sizeA + sizeB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Integer> bigrams(String s) {
|
||||||
|
Map<String, Integer> map = new HashMap<>();
|
||||||
|
for (int i = 0; i < s.length() - 1; i++) {
|
||||||
|
String gram = s.substring(i, i + 2);
|
||||||
|
map.merge(gram, 1, Integer::sum);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
117
docs/design/343-frontend-test-infra/README.md
Normal file
117
docs/design/343-frontend-test-infra/README.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# 설계서: RTL/Jest 인프라 + next/image + ARIA Tabs (#343)
|
||||||
|
|
||||||
|
> **상태**: Approved
|
||||||
|
> **작성**: [AI] Architect · **최종수정**: 2026-06-15
|
||||||
|
> **추적성** — Redmine: #343 · 부모: #281 (현행화 frontend-review-memo, 09-Done)
|
||||||
|
> · 구현 파일: `frontend/package.json`, `frontend/jest.config.ts` (신규), `frontend/jest.setup.ts` (신규), `frontend/next.config.ts`, `frontend/__tests__/*` (신규), `frontend/src/components/MyReviewsList.tsx`, `frontend/src/components/ReviewSection.tsx`
|
||||||
|
> · 테스트: 본 이슈가 테스트 인프라 자체를 도입
|
||||||
|
|
||||||
|
## 1. 목적 (Why)
|
||||||
|
|
||||||
|
본 프로젝트는 지금까지 자동화된 단위 테스트가 0건. 누적된 "후속 테스트" 항목이 12+개(StatsService/CacheService/SearchService/Stars/HangulSimilarity 등)이며, 그동안 분리된 후속 이슈를 처리할 인프라가 없어 모두 보류 상태. 본 이슈에서 Jest + RTL 인프라 도입 + next/image 적용 + ARIA Tabs 보강.
|
||||||
|
|
||||||
|
## 2. 범위 (Scope)
|
||||||
|
|
||||||
|
- **포함**
|
||||||
|
- Jest 30 + `next/jest` 자동 설정 + `@testing-library/react` + `@testing-library/jest-dom`.
|
||||||
|
- `jest.config.ts`, `jest.setup.ts`, `package.json scripts: test, test:watch`.
|
||||||
|
- 샘플 테스트 3개 — 가장 안전한 순수 함수/단순 컴포넌트로 인프라 검증:
|
||||||
|
- `Stars` 컴포넌트 렌더 + 별점 표시
|
||||||
|
- 기존 `HangulSimilarity` (#348) — 자모/유사도
|
||||||
|
- `BotDetector` (#337) — 봇 UA 패턴
|
||||||
|
- `next.config.ts` `images.remotePatterns`에 Google avatar 도메인(`lh3.googleusercontent.com`) + YouTube thumbnail(`i.ytimg.com`).
|
||||||
|
- `ReviewSection`/`MyReviewsList`의 `<img>` 일부를 `next/image` 또는 명시적 eslint-disable로 정리.
|
||||||
|
- `MyReviewsList` 탭에 WAI-ARIA Tabs 패턴(role=tablist/tab/aria-selected/aria-controls).
|
||||||
|
- **제외 (후속)**
|
||||||
|
- 백엔드 JUnit 테스트 인프라 (별도 큰 작업).
|
||||||
|
- E2E (Playwright) 도입.
|
||||||
|
- CI 통합 (GitHub Actions 또는 OCI DevOps).
|
||||||
|
- 모든 컴포넌트 테스트 — 점진적으로 추가.
|
||||||
|
- 모든 `<img>` → `next/image` 전수 교체 — 점진적.
|
||||||
|
|
||||||
|
## 3. 인수조건
|
||||||
|
|
||||||
|
- [ ] `npm test`가 단일 명령으로 동작 (0건 → 샘플 3개 통과).
|
||||||
|
- [ ] `npm run build`가 회귀 없이 통과.
|
||||||
|
- [ ] `next.config.ts`에 `remotePatterns` 설정.
|
||||||
|
- [ ] `ReviewSection`의 user_avatar_url `<img>`에 `next/image` 또는 eslint-disable 주석.
|
||||||
|
- [ ] `MyReviewsList` 탭이 `role="tablist"`/`role="tab"`/`aria-selected`/`aria-controls`/`tabIndex` 설정.
|
||||||
|
|
||||||
|
## 4. 컨텍스트 & 제약
|
||||||
|
|
||||||
|
- Next.js 16.1.6 + Turbopack.
|
||||||
|
- `next/jest`는 SWC/Babel 자동 통합. Turbopack 빌드와는 분리(테스트만 Jest 별도).
|
||||||
|
- Pretendard/Geist 폰트는 `next/font/local` 사용 → 테스트에선 mock 불필요.
|
||||||
|
- 패널 분리(#329)로 admin 영역은 단위 테스트 도입 더 쉬워짐.
|
||||||
|
|
||||||
|
## 5. 아키텍처 개요
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── package.json
|
||||||
|
│ └── scripts: test, test:watch
|
||||||
|
├── jest.config.ts (next/jest createNextJestConfig 사용)
|
||||||
|
├── jest.setup.ts (@testing-library/jest-dom 확장 matchers)
|
||||||
|
├── __tests__/
|
||||||
|
│ ├── Stars.test.tsx
|
||||||
|
│ ├── HangulSimilarity.test.ts (자체 구현은 backend Java, TS 포팅은 미적용 → 다른 순수 함수로 대체)
|
||||||
|
│ └── BotDetector.test.ts (마찬가지 — backend → TS 동등 포팅 불가)
|
||||||
|
└── (대안) 프론트 측 순수 함수:
|
||||||
|
├── lib/cuisine-icons.ts 의 getPhosphorCuisineIcon
|
||||||
|
├── components/Stars 의 0.5 단위 렌더
|
||||||
|
└── i18n/config.ts 의 isLocale/detectBrowserLocale
|
||||||
|
```
|
||||||
|
|
||||||
|
→ 백엔드 Java 코드는 TS 테스트로 검증 불가. 프론트 측 순수 함수 3개로 대체:
|
||||||
|
- `Stars` 렌더 (RTL component test)
|
||||||
|
- `i18n/config.ts` `isLocale` (pure)
|
||||||
|
- `i18n/config.ts` `detectBrowserLocale` (navigator mock)
|
||||||
|
|
||||||
|
## 6. 데이터 모델
|
||||||
|
|
||||||
|
`__tests__/*.test.{tsx,ts}` — Jest 표준 컨벤션.
|
||||||
|
|
||||||
|
## 7. 함수 명세
|
||||||
|
|
||||||
|
| 단위 | 책임 | 비고 |
|
||||||
|
|------|------|------|
|
||||||
|
| `jest.config.ts` | `createJestConfig(customConfig)` + moduleNameMapper `@/*` | next/jest |
|
||||||
|
| `jest.setup.ts` | `import "@testing-library/jest-dom"` | 확장 matchers |
|
||||||
|
| `Stars.test.tsx` | 별점 0/2.5/5 렌더, aria-label 확인 | RTL |
|
||||||
|
| `i18n/config.test.ts` | isLocale/detectBrowserLocale | navigator mock |
|
||||||
|
| `MyReviewsList` Tabs 패치 | tablist/tab/aria-selected | role + aria |
|
||||||
|
| `ReviewSection` img → eslint-disable | 최소 변경 | next/image는 후속 |
|
||||||
|
|
||||||
|
## 8. 흐름
|
||||||
|
|
||||||
|
1. `npm i -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event @types/jest`.
|
||||||
|
2. `jest.config.ts` + `jest.setup.ts` 작성.
|
||||||
|
3. `package.json`에 `"test": "jest"` + `"test:watch": "jest --watch"` 추가.
|
||||||
|
4. `__tests__/`에 3개 샘플 테스트.
|
||||||
|
5. `next.config.ts`에 `remotePatterns` 추가.
|
||||||
|
6. `MyReviewsList` Tabs ARIA 보강.
|
||||||
|
7. `ReviewSection`의 `<img>` 라인에 `// eslint-disable-next-line @next/next/no-img-element` (next/image 전환은 후속).
|
||||||
|
8. `npm test` 통과 → `npm run build` 통과.
|
||||||
|
|
||||||
|
## 9. 엣지케이스
|
||||||
|
|
||||||
|
- **Turbopack vs Jest**: 무관 (테스트는 별도 SWC 컴파일).
|
||||||
|
- **CSS modules / globals.css import**: jest.config.ts의 moduleNameMapper로 `\\.(css|scss)$` → `identity-obj-proxy` 대신 next/jest가 자동 처리.
|
||||||
|
- **Next.js Server Components**: 본 프로젝트는 모두 `"use client"` 컴포넌트라 RTL이 통상 동작.
|
||||||
|
|
||||||
|
## 10. 테스트
|
||||||
|
|
||||||
|
자기 자신 — `npm test`가 통과해야 본 이슈 완료.
|
||||||
|
|
||||||
|
## 11. 리스크 & 대안
|
||||||
|
|
||||||
|
- **선택**: `next/jest` + RTL. Next.js 공식 권장.
|
||||||
|
- **대안 A**: Vitest — 더 빠르지만 Next.js 공식 가이드 부재, 본 프로젝트 규모에서 차이 작음.
|
||||||
|
- **대안 B**: Playwright Component Testing — 더 무겁고 E2E 통합 안 됨.
|
||||||
|
- **트레이드오프**: Jest 30 + RTL은 React 19에 호환. 의존성 부담은 dev-only.
|
||||||
|
|
||||||
|
## 12. 미해결 질문
|
||||||
|
|
||||||
|
- CI(테스트 자동 실행) — 본 이슈 범위 밖. OCI DevOps Build Pipeline은 ARM64 미지원 → GitHub Actions 또는 Gitea Actions 후속.
|
||||||
|
- 백엔드 JUnit 테스트 인프라 — 별도 큰 이슈.
|
||||||
|
- E2E (Playwright) — 별도.
|
||||||
80
docs/design/348-name-similarity/README.md
Normal file
80
docs/design/348-name-similarity/README.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# 설계서: isNameSimilar 한국어 자모 분해 + Sørensen-Dice (#348)
|
||||||
|
|
||||||
|
> **상태**: Approved
|
||||||
|
> **작성**: [AI] Architect · **최종수정**: 2026-06-15
|
||||||
|
> **추적성** — Redmine: #348 · 부모: #332 (이미 close)
|
||||||
|
> · 구현 파일: `backend-java/src/main/java/com/tasteby/util/HangulSimilarity.java` (신규), `backend-java/src/main/java/com/tasteby/controller/RestaurantController.java`
|
||||||
|
|
||||||
|
## 1. 목적
|
||||||
|
|
||||||
|
기존 `isNameSimilar`가 Jaccard-like(문자 set 교집합 비율 ≥ 0.4)로 짧은 한국어 이름에서 오탐 가능. 자모 분해 + Sørensen-Dice bigram으로 정확도 향상.
|
||||||
|
|
||||||
|
## 2. 범위
|
||||||
|
|
||||||
|
- **포함**
|
||||||
|
- `HangulSimilarity.similarity(a, b)` 유틸 신규
|
||||||
|
- `RestaurantController.isNameSimilar` 호출부를 새 유틸로 교체
|
||||||
|
- **제외 (별도 후속으로 분리)**
|
||||||
|
- DDG → 정식 검색 API 전환 (외부 API 결정 + 비용/계약 필요)
|
||||||
|
- DTO RestaurantUpdateDTO + @Valid 표준화 (#332 화이트리스트 set으로 SQL 측 가드 확보)
|
||||||
|
- UNIQUE(google_place_id) 제약 강화 — DB 중복 정리 선행 필요(현재 10+건 중복 확인)
|
||||||
|
|
||||||
|
## 3. 인수조건
|
||||||
|
|
||||||
|
- [ ] `HangulSimilarity.similarity(a, b)` 0.0~1.0 반환 (1.0=동일)
|
||||||
|
- [ ] 한국어 음절을 Unicode NFD로 자모 분해(초성·중성·종성)
|
||||||
|
- [ ] 분해 후 bigram 기반 Sørensen-Dice 계수 계산
|
||||||
|
- [ ] 빈 문자열 안전 처리 (둘 다 비면 0.0, 한쪽만 비면 0.0)
|
||||||
|
- [ ] `RestaurantController.isNameSimilar` 임계값 0.45로 호출 (Jaccard 0.4와 유사 보수성)
|
||||||
|
- [ ] 회귀 없음 — 기존 정상 매칭 시나리오 통과
|
||||||
|
|
||||||
|
## 4. 컨텍스트 & 제약
|
||||||
|
|
||||||
|
- Java 21 `Normalizer.normalize(Form.NFD)` 활용.
|
||||||
|
- 한글 음절(가-힣) NFD → 초성(ㄱ-ㅎ 호환자모 또는 조합자모) + 중성 + 종성.
|
||||||
|
- 영문/숫자는 그대로 통과.
|
||||||
|
- Sørensen-Dice: `2 * |A ∩ B| / (|A| + |B|)` — bigram 다중집합(multiset) 기준.
|
||||||
|
|
||||||
|
## 5. 함수 명세
|
||||||
|
|
||||||
|
| 함수 | 책임 | 시그니처 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `decomposeHangul(s)` | NFD 자모 분해 + 공백/구두점 제거 + 소문자화 | `static String decompose(String)` |
|
||||||
|
| `bigrams(s)` | 2글자 bigram 리스트 | `static List<String> bigrams(String)` |
|
||||||
|
| `similarity(a, b)` | Sørensen-Dice 0.0~1.0 | `static double similarity(String, String)` |
|
||||||
|
|
||||||
|
## 6. 흐름
|
||||||
|
|
||||||
|
1. 두 이름을 `decompose`로 자모 분해 + 정규화.
|
||||||
|
2. 각 분해 결과를 `bigrams`로 분해.
|
||||||
|
3. multiset 교집합 크기 카운트.
|
||||||
|
4. `2 * common / (sizeA + sizeB)`.
|
||||||
|
|
||||||
|
## 7. 엣지케이스
|
||||||
|
|
||||||
|
- **둘 다 빈 문자열**: 0.0 반환.
|
||||||
|
- **bigram 1개 이하**: 두 문자열 같으면 1.0, 아니면 0.0.
|
||||||
|
- **포함 관계**: 기존 코드의 `a.contains(b) || b.contains(a)` 단축 평가 유지 (1.0 반환).
|
||||||
|
- **혼합(한영)**: NFD가 한글만 분해 → 영문은 그대로. bigram 계산은 동일하게 동작.
|
||||||
|
|
||||||
|
## 8. 테스트 (수동)
|
||||||
|
|
||||||
|
```
|
||||||
|
similarity("스타벅스 강남", "스타벅스 강남점") → ≥ 0.85
|
||||||
|
similarity("스타벅스 강남", "스타벅스 종로") → ≥ 0.55, < 0.85
|
||||||
|
similarity("스타벅스", "맥도날드") → < 0.20
|
||||||
|
similarity("PIZZAHUT", "피자헛") → 한글 + 영문 혼재 가드 통과
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 리스크 & 대안
|
||||||
|
|
||||||
|
- **선택**: NFD 분해 + bigram Sørensen-Dice. Java 표준 라이브러리만 사용.
|
||||||
|
- **대안 A**: Apache Commons Text `JaroWinklerSimilarity` — 라이브러리 추가 부담.
|
||||||
|
- **대안 B**: Hangul.js류 라이브러리 — Java 포팅 없음.
|
||||||
|
- **대안 C**: Levenshtein 거리 — 자모 분해와 결합 시 좋으나 구현 복잡.
|
||||||
|
|
||||||
|
## 10. 미해결 질문 / 분리된 후속
|
||||||
|
|
||||||
|
- DDG → 정식 검색 API: Naver Search API 또는 Google Custom Search (외부 API 결정 + 비용 검토 필요) — 별도 신규 이슈
|
||||||
|
- DTO RestaurantUpdateDTO + @Valid: #332 set 화이트리스트로 1차 가드. 본격 DTO는 큰 변경 — 별도 신규 이슈
|
||||||
|
- UNIQUE(google_place_id) 제약: 현재 10+건 중복. 데이터 정리(병합/삭제) 선행 → 별도 신규 이슈
|
||||||
Reference in New Issue
Block a user