Compare commits

..

5 Commits

Author SHA1 Message Date
joungmin
7b2753b9fd docs(design): #329 admin/page.tsx 분리 + 유틸 통일 설계서 (Architect)
5개 패널을 _panels/<Panel>.tsx로 추출 + localStorage→getAdminToken + SSE→consumeSseStream.
내부 로직 변경 없음(점진적 접근).

설계서: docs/design/329-admin-split/README.md (Approved)
Refs: #329 (Architect)
2026-06-15 15:48:49 +09:00
joungmin
7411c8956f docs(changelog): v0.1.34 #331 VectorService batchUpdate 기록 2026-06-15 15:42:43 +09:00
joungmin
be302612f5 perf(vector): #331 VectorService.saveRestaurantVectors batchUpdate
- N+1 단건 jdbc.update → 단일 jdbc.batchUpdate(SqlParameterSource[])
- UUID 인라인 변환 → IdGenerator.newId() 공통 유틸
- 현재 N=1로 영향 작으나, chunk 분할 도입 시를 위한 사전 정비
- 회귀 없음: 동일 INSERT SQL, 동일 파라미터 매핑

설계서: docs/design/331-vector-batch-insert/README.md

Refs: #331 (Developer 단계)
2026-06-15 15:40:45 +09:00
joungmin
91d9813253 docs(design): #331 VectorService batchUpdate 설계서 (Architect)
jdbc.batchUpdate(SqlParameterSource[]) 단일 호출 + IdGenerator.newId() 공통화.
테스트는 본 범위 밖 (#343 후속 테스트 인프라).

설계서: docs/design/331-vector-batch-insert/README.md (Approved)
Refs: #331 (Architect)
2026-06-15 15:39:50 +09:00
joungmin
11e1cf7877 docs(changelog): v0.1.33 #326 parseJson 단일 패스 기록 2026-06-15 15:37:48 +09:00
4 changed files with 217 additions and 9 deletions

View File

@@ -6,6 +6,20 @@
## 2026-06-15
### ⚡ #331 VectorService batchUpdate (v0.1.34)
- saveRestaurantVectors: N+1 단건 INSERT → 단일 jdbc.batchUpdate(SqlParameterSource[])
- UUID 인라인 변환 제거 → IdGenerator.newId() 공통화
- 현재 N=1이지만 chunk 분할 도입 시 효과 본격화
- 설계서: docs/design/331-vector-batch-insert/README.md
- Refs: #331 (close)
### ⚡ #326 parseJson 단일 패스 (v0.1.33)
- OciGenAiService.parseJson 잘린 배열 복구를 brace depth counter 단일 패스로 교체
- 이전 O(N²) + Jackson 예외 양산 → O(N) + 명시적 에러 경로
- 문자열/escape 처리 정확
- 설계서: docs/design/326-parsejson-optimization/README.md
- Refs: #326 (close)
### 🛡️ #332 Restaurant PUT 화이트리스트 명시 (v0.1.32)
- ALLOWED_UPDATE_FIELDS set으로 PUT /api/restaurants/{id} body 필터
- 허용 외 키 silent drop + DEBUG 로그

View File

@@ -1,10 +1,12 @@
package com.tasteby.service;
import com.tasteby.util.IdGenerator;
import com.tasteby.util.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.stereotype.Service;
import java.util.*;
@@ -64,6 +66,9 @@ public class VectorService {
/**
* Save vector embeddings for a restaurant.
*
* #331 — N개 청크를 단일 batchUpdate 호출로 처리 (이전: N+1 INSERT round-trip).
* UUID 생성은 IdGenerator.newId() 공통 유틸 사용 (인라인 변환 코드 제거).
*/
public void saveRestaurantVectors(String restaurantId, List<String> chunks) {
if (chunks.isEmpty()) return;
@@ -75,19 +80,20 @@ public class VectorService {
VALUES (:id, :rid, :chunk, :emb)
""";
SqlParameterSource[] batch = new SqlParameterSource[chunks.size()];
for (int i = 0; i < chunks.size(); i++) {
String id = UUID.randomUUID().toString().replace("-", "").substring(0, 32).toUpperCase();
float[] vec = new float[embeddings.get(i).size()];
List<Double> emb = embeddings.get(i);
float[] vec = new float[emb.size()];
for (int j = 0; j < vec.length; j++) {
vec[j] = embeddings.get(i).get(j).floatValue();
vec[j] = emb.get(j).floatValue();
}
var params = new MapSqlParameterSource();
params.addValue("id", id);
params.addValue("rid", restaurantId);
params.addValue("chunk", chunks.get(i));
params.addValue("emb", vec);
jdbc.update(sql, params);
batch[i] = new MapSqlParameterSource()
.addValue("id", IdGenerator.newId())
.addValue("rid", restaurantId)
.addValue("chunk", chunks.get(i))
.addValue("emb", vec);
}
jdbc.batchUpdate(sql, batch);
}
/**

View File

@@ -0,0 +1,107 @@
# 설계서: admin/page.tsx 분리 + 토큰/SSE 유틸 통일 (#329)
> **상태**: Approved
> **작성**: [AI] Architect · **최종수정**: 2026-06-15
> **추적성** — Redmine: #329 · 부모: #304 (현행화 frontend-admin, 09-Done)
> · 구현 파일: `frontend/src/app/admin/page.tsx`, `frontend/src/app/admin/_panels/*.tsx` (신규), `frontend/src/lib/admin-utils.ts`
> · 테스트: 수동 (각 탭 진입 + 기존 시나리오 회귀)
## 1. 목적 (Why)
`admin/page.tsx`가 2817 LOC 단일 파일. 5개 패널이 같은 파일 안에 함께 있어 (a) 빌드 변경 시 무관한 패널까지 재빌드/재배포, (b) 코드 리뷰 시 충돌 가능성↑, (c) 로직 격리 어려움. 또한 `localStorage` 직접 호출 + SSE 파싱 코드 중복이 남아있어 #304 후속에서 한 번 더 정리.
## 2. 범위 (Scope)
- **포함**
- 5개 패널을 `app/admin/_panels/<Panel>.tsx`로 추출 (Next.js underscore prefix로 라우팅 제외).
- `page.tsx`는 탭 라우팅 + 헤더 + 패널 import만.
- 남은 `localStorage.getItem("tasteby_token")` 호출 → `getAdminToken()`/`authHeaders()` 교체.
- SSE 파싱 중복(약 5~6곳) → `consumeSseStream()` 활용.
- 공유 타입(`AdminUser`/`UserFavorite`/`UserReview`/`UserMemo`/`VideoSortKey`)을 _panels 파일 내부 또는 `api.ts`에 옮김(짧은 타입만).
- **제외**
- 패널 내부 로직 변경 (state/effect 그대로).
- catch{/*ignore*/} 일괄 로깅 (별도 후속).
- ad-hoc 타입 캐스팅 정리 (별도 후속).
- 디자인 시스템 색상 통일.
## 3. 인수조건
- [ ] 5개 파일 신규: `_panels/{Channels,Videos,Restaurants,Users,Daemon}Panel.tsx`.
- [ ] `page.tsx` < 200 LOC (이전 2817).
- [ ] localStorage 직접 호출이 admin 페이지 내에 0건.
- [ ] SSE reader 직접 호출(`response.body?.getReader()`)이 0건.
- [ ] 모든 탭 진입 + 기존 시나리오 회귀 없음(빌드 통과 + 수동 smoke).
- [ ] dev 빌드 + 운영 배포 성공.
## 4. 컨텍스트 & 제약
- Next.js 16 (App Router). `_` prefix 디렉토리는 route 제외.
- `"use client"` 지시문 각 패널 파일 상단에 필요.
- React Server Components 미사용 (모두 클라이언트 컴포넌트).
- `useAuth()` 훅이 `auth-context`에 있음. 부모 `AdminPage`가 isAdmin 판정 후 prop으로 패널에 전달.
- 패널 간 상태 공유 없음 (각각 독립).
## 5. 아키텍처 개요
```
app/admin/
page.tsx ← 탭 라우팅 + 헤더 + CacheFlushButton + 패널 import
_panels/
ChannelsPanel.tsx ← 214 LOC
VideosPanel.tsx ← 1272 LOC (가장 큼)
RestaurantsPanel.tsx ← 667 LOC
UsersPanel.tsx ← 332 LOC
DaemonPanel.tsx ← 223 LOC
lib/admin-utils.ts (이미 존재)
├─ getAdminToken()
├─ authHeaders()
└─ consumeSseStream()
```
## 6. 함수 명세
| 단위 | 책임 | 비고 |
|------|------|------|
| `page.tsx` (재작성) | 탭 라우팅 + 패널 import | < 200 LOC |
| `_panels/*.tsx` (신규 5개) | 각 패널 로직 그대로 옮김 | "use client" |
| `localStorage` 호출 (~10곳) | `getAdminToken()`/`authHeaders()`로 통일 | 의미 동일 |
| SSE `getReader` (~5곳) | `consumeSseStream(resp, onEvent)`로 통일 | 의미 동일 |
## 7. 흐름
1. `_panels/` 디렉토리 생성.
2. 각 패널 함수 + 그에 종속된 타입/상수를 `<Panel>Panel.tsx`로 잘라 옮김.
3. 각 파일에 `"use client"` + import 추가.
4. `localStorage.getItem("tasteby_token")``getAdminToken()` 일괄.
5. SSE `getReader/decoder/buf.split/match` 패턴 → `consumeSseStream(resp, onEvent)` 일괄.
6. `page.tsx` 재작성 — 탭 라우팅 + 패널 import.
7. `npm run build`.
8. `pm2 restart` 또는 `deploy.sh --frontend-only`.
## 8. 엣지케이스
- **순환 import**: 패널 간 의존 없음 → 안전.
- **Type 중복**: `AdminUser` 등 패널 내부 타입은 그대로 옮김. 공유 타입은 `api.ts`에 있음.
- **default export vs named**: 각 패널은 named export. `page.tsx`에서 `import { ChannelsPanel } from "./_panels/ChannelsPanel"`.
- **빌드 크기**: 동일(코드 splitting은 별도 작업).
## 9. 테스트
- 빌드: `npm run build` 통과.
- 수동:
- `/admin` 접근 → 5탭 모두 진입 가능.
- 채널 추가, 영상 강제 추출(SSE), 식당 검색/수정, 유저 권한 토글, 데몬 설정 변경 — 모두 정상.
- 자동: 별도 후속(테스트 인프라 #343).
## 10. 리스크 & 대안
- **선택**: 5개 파일 추출 + 내부 로직 그대로.
- **대안 A**: 추출 + 내부 리팩터링 동시 — 회귀 위험↑, 별도 후속이 안전.
- **대안 B**: Atomic Design (atoms/molecules/organisms) — 큰 재구조화. 미루기.
- **트레이드오프**: 외형은 동일, 유지보수성/충돌 가능성만 개선. 점진적 접근.
## 11. 미해결 질문
- 공통 panel layout(헤더/리스트/페이징) 추상화 — 후속.
- VideosPanel 1272 LOC 내부 분할(상태 머신 + SSE 흐름 분리) — 별도 후속.

View File

@@ -0,0 +1,81 @@
# 설계서: VectorService batch insert + IdGenerator 공통화 (#331)
> **상태**: Approved
> **작성**: [AI] Architect · **최종수정**: 2026-06-15
> **추적성** — Redmine: #331 · 부모: #293 (검색/벡터 Reviewer 후속, 09-Done)
> · 구현 파일: `backend-java/src/main/java/com/tasteby/service/VectorService.java`
> · 테스트: 본 이슈 범위 밖 (단위 테스트 인프라 도입은 #343 후속 묶음에 해당)
## 1. 목적 (Why)
`VectorService.saveRestaurantVectors`가 chunk N개를 N번의 단건 `jdbc.update`로 처리한다. 현재 `buildChunks`가 1개 청크만 반환해 N=1이지만, 향후 chunk 분할 도입 시 N+1 INSERT 비효율. 또한 UUID 생성 코드가 인라인 변환(`UUID.randomUUID().toString().replace("-", "").substring(0, 32).toUpperCase()`)으로 다른 곳의 `IdGenerator.newId()`와 중복.
## 2. 범위
- **포함**
- `jdbc.batchUpdate(sql, SqlParameterSource[])`로 단일 호출 전환.
- UUID 생성을 `IdGenerator.newId()` 공통 유틸로 교체.
- **제외**
- 단위/통합 테스트 도입 (테스트 인프라 미도입 — 별도 후속 #343 묶음).
- `buildChunks`의 chunk 분할 로직 자체 변경 (현재 단일 청크 정책 유지).
- `restaurant_vectors` 스키마 변경.
## 3. 인수조건
- [ ] `saveRestaurantVectors`가 한 번의 `jdbc.batchUpdate` 호출로 N개 청크 삽입.
- [ ] UUID 인라인 변환 제거 → `IdGenerator.newId()` 호출.
- [ ] 회귀 없음 — 신규 식당 등록 시 `restaurant_vectors`에 정상 row 추가.
- [ ] N=0 가드(`chunks.isEmpty()`)는 유지.
## 4. 컨텍스트 & 제약
- Spring `NamedParameterJdbcTemplate.batchUpdate(String, SqlParameterSource[])` 사용.
- Oracle VECTOR 타입 파라미터는 `float[]`로 그대로 바인딩 가능 (`MapSqlParameterSource.addValue`).
- 한 batch 안 `int[]` 반환 → batch 결과 카운트는 사용하지 않음(throw if 어쩌고 미적용).
- `IdGenerator.newId()` 시그니처: `public static String newId()` → 32-char uppercase hex (현재 인라인과 동일).
## 5. 아키텍처 개요
```
saveRestaurantVectors(restaurantId, chunks)
├ if chunks.isEmpty() → return
├ embeddings = genAi.embedTexts(chunks)
├ params[] = build N개 MapSqlParameterSource
│ .addValue("id", IdGenerator.newId())
│ .addValue("rid", restaurantId)
│ .addValue("chunk", chunks.get(i))
│ .addValue("emb", float[] embeddings[i])
└ jdbc.batchUpdate(sql, params)
```
## 6. 함수 명세
| 함수 | 책임 | 비고 |
|------|------|------|
| `VectorService.saveRestaurantVectors(id, chunks)` (수정) | batchUpdate 1회 | IdGenerator 사용 |
## 7. 흐름
1. embed 호출 (기존).
2. `SqlParameterSource[]` 생성.
3. `jdbc.batchUpdate(sql, params)` 단일 호출.
## 8. 엣지케이스
- **chunks 빈 배열**: 조기 return (기존 유지).
- **embed 결과와 chunks 크기 불일치**: 현재 OCI GenAI는 입력 N → 출력 N 보장. 안전 가드 추가는 본 범위 밖 (필요 시 후속).
## 9. 테스트 (수동만)
- dev에서 신규 식당 등록(데몬 또는 수동 trigger) → `SELECT count(*) FROM restaurant_vectors WHERE restaurant_id = '...'` 정상 row 확인.
## 10. 리스크 & 대안
- **선택**: `NamedParameterJdbcTemplate.batchUpdate`. 단일 트랜잭션 + 단일 round-trip.
- **대안 A**: `JdbcTemplate.batchUpdate(BatchPreparedStatementSetter)` — 더 저수준이지만 named param 손실.
- **대안 B**: MERGE로 upsert — 동일 restaurant_id 재처리 시 중복 제거 가능. 다만 본 이슈 범위 밖.
## 11. 미해결 질문
- chunk 분할 정책(현재 1개 단일 청크) — 후속 (검색 정확도 vs 토큰 비용 트레이드오프 결정).
- batchUpdate 결과 row 수 검증 — 운영 모니터링 도구 도입 후 결정.