diff --git "a/static/02_\354\240\225\354\261\205_\354\240\225\354\235\230\354\204\234.md" "b/static/02_\354\240\225\354\261\205_\354\240\225\354\235\230\354\204\234.md" index 0b16332..30bfba0 100644 --- "a/static/02_\354\240\225\354\261\205_\354\240\225\354\235\230\354\204\234.md" +++ "b/static/02_\354\240\225\354\261\205_\354\240\225\354\235\230\354\204\234.md" @@ -117,10 +117,14 @@ Keyword 등급별 계산 범위와 처리 방식은 [AI 설계](05_AI_설계.md) ## 9. Feed - Feed의 추천 단위는 발행된 Collection입니다. +- 추천 후보는 **최신 발행·팔로우·탐색용 무작위** 세 경로에서 만듭니다. AI Keyword가 아직 없는 Collection도 후보에 포함하며 Keyword는 빈 목록으로 표시합니다. +- 추천 순서는 **팔로우·공개 Keyword 적합도·최신성**으로 정하고 이미 노출된 Collection에는 감점을 둡니다. 추천 계산은 요청 시점의 규칙 기반이며 학습 모델을 쓰지 않습니다. - 탈퇴 User와 모든 소프트 삭제 데이터는 노출하지 않습니다. - 다른 사용자의 Context 원문은 제공하지 않습니다. - 신고·차단과 개별 Record 추천은 MVP에서 제공하지 않습니다. +Feed 정책과 계약은 AI 파트가 소유하고 런타임 구현은 Backend 파트가 담당합니다. 세부 계약은 [AI 설계](05_AI_설계.md) 14장에 있습니다. + ## 10. 삭제 및 탈퇴 회원 탈퇴 시 User, SocialAccount, Record, Context, Collection, CollectionRecord, Shelf 및 관련 Follow를 소프트 삭제합니다. diff --git "a/static/04_\354\235\265\353\252\205SNS_\352\263\265\352\260\234\354\240\225\354\261\205.md" "b/static/04_\354\235\265\353\252\205SNS_\352\263\265\352\260\234\354\240\225\354\261\205.md" index 803364a..9db8a77 100644 --- "a/static/04_\354\235\265\353\252\205SNS_\352\263\265\352\260\234\354\240\225\354\261\205.md" +++ "b/static/04_\354\235\265\353\252\205SNS_\352\263\265\352\260\234\354\240\225\354\261\205.md" @@ -43,6 +43,8 @@ Keyword는 사전 정의된 프리셋에서만 선택하며, 타인에게 공개 - 추천 단위는 Collection입니다. - 공개 화면에서 다른 사용자의 Context 원문을 제공하지 않습니다. +- **타인 Collection의 추천 특징으로 쓰는 Keyword는 `PUBLIC`뿐입니다.** `PRIVATE_ONLY`는 본인 관심 계산에만 쓰고, `BLOCKED`는 계산과 노출 모두에서 제외합니다(2장 표와 같습니다). +- **Feed 응답에 소유자를 식별할 수 있는 값을 넣지 않습니다.** 같은 Shelf를 따라가는 경로는 있어도 소유자 신원은 드러나지 않습니다. - 소프트 삭제된 User, Shelf, Collection, Record, CollectionRecord는 제외합니다. - 탈퇴한 User의 Collection은 Feed에서 즉시 제외합니다. - 신고·차단은 MVP에 포함하지 않습니다. diff --git "a/static/05-1_\355\214\214\355\212\270\352\260\204_\354\232\224\352\265\254\354\202\254\355\225\255.md" "b/static/05-1_\355\214\214\355\212\270\352\260\204_\354\232\224\352\265\254\354\202\254\355\225\255.md" index c39e661..b2f35b5 100644 --- "a/static/05-1_\355\214\214\355\212\270\352\260\204_\354\232\224\352\265\254\354\202\254\355\225\255.md" +++ "b/static/05-1_\355\214\214\355\212\270\352\260\204_\354\232\224\352\265\254\354\202\254\355\225\255.md" @@ -32,11 +32,13 @@ AI 분석은 저장 이후 비동기로 진행되므로, Keyword가 아직 없 ### 1.3 Feed 이벤트 전송 (필수) -Feed 추천 결과에 대한 이벤트 중 **`CLICK`·`SAVE`는 Front가 전송**합니다([API 명세](08_API_명세.md)의 `POST /feed/events`). +Feed 추천 결과에 대한 이벤트 중 **`CLICK`·`SAVE`는 Front가 전송**합니다([API 명세](08_API_명세.md) 10.2의 `POST /feed/events`). -- payload에 `requestId`와 `position`을 포함합니다. -- `IMPRESSION`은 Feed 응답 생성 시 서버가 직접 기록하므로 Front가 전송하지 않습니다. -- 수집 항목과 저장 구조는 back 파트의 Feed 문서를 따릅니다. +- **이벤트는 배열로 묶어 보냅니다.** 이벤트마다 요청을 보내지 않습니다. 배열 크기 상한은 100개입니다(1.5). +- `requestId`는 **배열 바깥의 별도 필드**입니다. 추천 목록 응답에서 받은 값을 그대로 돌려보냅니다. 개별 이벤트 항목 안에 다시 넣지 않습니다. +- 개별 이벤트 항목에는 `event`·`collectionId`·`placeId`·`position`을 담고, `position`도 목록 응답에서 받은 값을 그대로 돌려보냅니다. +- `IMPRESSION`은 Feed 응답 생성 시 서버가 직접 기록하므로 Front가 전송하지 않습니다. 보내면 `400`입니다. +- 저장 구조와 집계 방식은 back 파트의 Feed 문서를 따릅니다([AI 설계](05_AI_설계.md) 17장). ### 1.4 Keyword 공개 등급 표시 (확인 필요) @@ -54,11 +56,13 @@ Keyword는 `PUBLIC` / `PRIVATE_ONLY` / `BLOCKED` 세 등급을 가집니다([익 |---|---|---| | Context 본문 | **500자** | `POST /records`(`contextBody`) · `POST /records/{recordId}/contexts`(`body`) · `PATCH /records/{recordId}/contexts/{contextId}`(`body`) | | `recordIds` 배열 | **100개** | `POST /collections` · `POST /collections/{collectionId}/records` | +| `events` 배열 | **100개** | `POST /feed/events` | Front는 다음을 지켜야 합니다. - **Context 입력 필드에 `maxlength=500`을 겁니다.** 서버만 막으면 사용자가 긴 글을 다 쓴 뒤에 거절당합니다. 저장 이유 메모는 한 번에 쓰는 성격이라 그 시점에 본문을 잃으면 체감이 나쁩니다. 남은 글자 수를 표시하는 편이 좋습니다. - **`recordIds`가 100개를 넘으면 나눠 호출합니다.** Record 추가는 멱등이므로([API 명세](08_API_명세.md) §7.5 — 이미 담긴 Record는 건너뛰고 나머지를 담습니다) 나눠 호출해도 중복이 문제되지 않습니다. +- **Feed 이벤트도 100개 단위로 끊어 보냅니다.** 같은 `requestId`의 이벤트가 100개를 넘으면 나눠 호출합니다. - 서버 400은 방어선으로 남으므로, 받았을 때 `error.fieldErrors`로 어떤 필드가 위반인지 표시할 수 있습니다. 값의 근거는 [데이터 모델](06_데이터모델_및_무결성.md) §8에 있습니다. diff --git "a/static/05_AI_\354\204\244\352\263\204.md" "b/static/05_AI_\354\204\244\352\263\204.md" index 3d7b373..63971d6 100644 --- "a/static/05_AI_\354\204\244\352\263\204.md" +++ "b/static/05_AI_\354\204\244\352\263\204.md" @@ -3,7 +3,7 @@ > 이 문서는 PinLog AI 시스템의 공용 아키텍처와 파트 간 계약을 정의하는 단일 원본입니다. > > FastAPI 내부 구현은 `Team-PinLog/ai/docs/`에서 관리합니다. -> Spring 연동과 Feed 구현은 `Team-PinLog/back/docs/`에서 관리합니다. +> Spring 연동과 Feed **구현**은 `Team-PinLog/back/docs/`에서 관리합니다. Feed **정책과 계약**은 이 문서 14장이 정본입니다. ## 1. 목적 @@ -38,7 +38,7 @@ AI는 Place·Record·Collection 기본 기능의 부가 계층입니다. AI가 12. Spring은 Core 도메인과 최종 응답을 담당합니다. 13. FastAPI는 AI 계산과 AI 파생 데이터 처리를 담당합니다. 14. FastAPI는 `core.*`를 직접 조회하거나 수정하지 않습니다. -15. Feed는 Spring이 담당하며 요청 시 LLM과 Embedding API를 호출하지 않습니다. +15. Feed 런타임 구현은 Spring이 담당하며 요청 시 LLM과 Embedding API를 호출하지 않습니다. Feed의 정책과 계약은 AI 파트가 소유합니다(14장). 16. GraphRAG와 RAG는 MVP에 포함하지 않습니다. ## 3. 시스템 구성과 책임 @@ -939,29 +939,71 @@ POST /internal/v1/search ## 14. Feed와 AI의 경계 -### 14.1 Feed 책임 +### 14.1 소유 경계 -Feed는 Spring Backend의 기능입니다. FastAPI는 Feed에 관여하지 않습니다. +| 구분 | 소유 파트 | 범위 | +|---|---|---| +| Feed **정책과 계약** | **AI 파트** | 후보 채널 구성, 점수 항 구성, Keyword Visibility 경계, 응답에 담을 수 있는 것, 외부 API 계약 | +| Feed **런타임 구현** | **Backend 파트(Spring)** | 채널별 쿼리, Redis Cache와 TTL, Feed Session, 이벤트 저장, 튜닝값 운영 | + +Feed는 Spring Backend의 기능입니다. FastAPI는 Feed에 관여하지 않으며 Feed API를 제공하지도, 점수를 계산하지도 않습니다. + +이 장은 Feed가 지켜야 할 정책과 계약을 정의합니다. 구현 명세는 `Team-PinLog/back/docs/ai/spec/feed-*.md`에서 관리하며(17장) 그 문서들은 이 장을 따릅니다. 둘이 어긋나면 이 문서가 정본입니다. + +### 14.2 후보 채널 + +MVP의 후보 채널은 세 개입니다. + +| 채널 | 목적 | 기본 배분 | +|---|---|---| +| 최신 발행 | 신규 Collection의 최소 노출 기회 | 100 | +| 팔로우 | 이미 관심을 표현한 Shelf | 80 | +| 탐색용 무작위 | 필터 버블 이탈 | 20 | + +- **MVP에서 Place category·region은 후보 채널로 쓰지 않습니다.** `core.place`가 두 값을 컬럼으로 저장하지 않고([데이터 모델](06_데이터모델_및_무결성.md) 2.3 — `category_group_code` 미저장, 지역 라벨은 주소 문자열에서 표시용으로만 파생), 도입하려면 DB·Front 계약과 백필이 함께 필요하기 때문입니다. 후속 과제로 미룹니다. +- **AI Keyword가 아직 없는 Collection도 후보에 포함합니다.** 응답에는 `keywords: []`를 넣으며 오류가 아닙니다. AI는 부가 계층이므로 AI 미완료가 노출 자격을 좌우하지 않습니다(핵심 원칙 9). +- 본인 소유 Collection은 모든 채널에서 제외합니다. + +배분값은 설정값이며 튜닝 대상입니다. 코드 상수가 아니라 외부 설정으로 둡니다. + +### 14.3 점수 계산 + +Feed 점수는 **요청 시점의 결정적(deterministic) 산술**입니다. 요청 경로에서 외부 모델을 호출하지 않고 벡터 유사도도 계산하지 않습니다(14.5). -### 14.2 Feed가 사용하는 AI 데이터 +점수는 네 항으로 구성합니다. -타인 Collection의 특징: +| 항 | 입력 | 기본 가중치 | +|---|---|---| +| 팔로우 신호 | 팔로우 채널 출처 여부 | 0.500 | +| Keyword 적합도 | 본인 Profile Keyword 분포 × Collection의 `PUBLIC` Keyword 분포 | 0.375 | +| 최신성 | `published_at` 경과 | 0.125 | +| 노출 패널티 | 최근 기간의 IMPRESSION 누적 | 감점 | + +- 가중치는 기존 공식에서 **Place category·region 항(`w_geo_cat`)을 제거한 뒤 나머지 세 항을 비례 정규화**한 값입니다. 상대 순서를 보존하고 합을 1.0으로 유지합니다. +- 팔로우가 가장 큰 이유는 사용자가 **명시적으로** 표현한 유일한 관심 신호이기 때문입니다. 추론값보다 명시값을 우선합니다. +- 노출 패널티는 이미 응답으로 나간 Collection을 반복해서 상위에 올리지 않기 위한 것이며, 상한을 두어 한 번 노출된 Collection이 영구히 배제되지 않게 합니다. +- **모든 수치는 설정값이며 튜닝 대상입니다.** 감쇠 함수·계수·집계 윈도우·다양성 조정·Cold Start 가중치는 back의 `feed-scoring.md`에서 관리합니다. + +### 14.4 Feed가 사용하는 AI 데이터 + +타인 Collection의 특징으로 사용할 수 있는 AI 데이터: - `PUBLIC` Keyword -- Place region -- Place category -사용자 개인 Profile: +사용자 개인 Profile 계산에 사용할 수 있는 AI 데이터: - 본인의 `PUBLIC` Keyword - 본인의 `PRIVATE_ONLY` Keyword -- 본인의 Place region·category 분포 -`BLOCKED`는 모든 계산에서 제외합니다. +`BLOCKED`는 계산과 노출 모두에서 제외합니다. + +`PRIVATE_ONLY`는 본인의 관심 Profile 계산에만 사용하고 타인 Collection의 특징으로는 사용하지 않습니다. 타인에게 보이지 않아야 할 정보가 추천 근거를 통해 드러나지 않게 하기 위한 구분입니다. 이 비대칭은 의도된 설계이며 8.3의 Visibility 표와 같습니다. + +제외는 **조회 쿼리에서** 수행합니다. 조회한 뒤 애플리케이션 코드에서 거르면 경로 하나만 빠뜨려도 노출됩니다. -`PRIVATE_ONLY`는 본인의 관심 Profile 계산에만 사용하고 타인 Collection의 특징으로는 사용하지 않습니다. 타인에게 보이지 않아야 할 정보가 추천 근거를 통해 드러나지 않게 하기 위한 구분입니다. +Feed 응답에는 **소유자를 식별할 수 있는 값을 포함하지 않습니다.** 소유자 식별자는 다양성 조정 같은 서버 내부 계산에만 사용합니다. -### 14.3 금지 사항 +### 14.5 금지 사항 Feed 요청 처리 중 다음을 호출하지 않습니다. @@ -969,9 +1011,23 @@ Feed 요청 처리 중 다음을 호출하지 않습니다. - Embedding API - LLM API +요청 시점에 벡터 유사도를 계산하지 않습니다. 후보 생성에도 벡터 검색을 쓰지 않습니다. + Feed는 이미 저장된 AI 파생 데이터와 Cache만 사용합니다. -Feed의 상세 구현은 back/docs에서 관리합니다. `feed_event`, IMPRESSION/CLICK/SAVE, Redis TTL, 후보 채널, 점수 공식, 노출 패널티, 다양성, Cold Start, Feed Session과 Pagination, Cache Stale 방어가 여기에 해당합니다. +### 14.6 API 계약 + +외부 API의 정본은 [API 명세](08_API_명세.md) 10장입니다. Feed 정책상 고정된 부분은 다음과 같습니다. + +- 추천 목록은 `GET /api/core/v1/feed/collections`이며 기본 `size`는 20, `cursor`는 내부 구조를 노출하지 않는 opaque 문자열입니다. +- **Collection 상세는 Feed 전용 URL을 만들지 않고 공통 `GET /api/core/v1/collections/{collectionId}`를 재사용합니다.** +- `requestId`는 Feed Session 식별자이며 **응답 본문과 `CLICK`·`SAVE` 이벤트 요청의 별도 필드**입니다. 개별 이벤트 항목 안에 중복해서 넣지 않습니다. +- `IMPRESSION`은 응답 생성 시 서버가 기록하고, 클라이언트는 `CLICK`·`SAVE`만 `POST /api/core/v1/feed/events`로 보냅니다. +- 이벤트 배열의 크기 상한은 **100**이며 `recordIds` 배열 상한과 같은 값입니다([파트간 요구사항](05-1_파트간_요구사항.md) 1.5). 요청 배열마다 상한을 따로 정하면 "서버 방어 상한이 얼마인가"에 답이 여러 개가 됩니다. + +### 14.7 back/docs에서 관리하는 것 + +`feed_event` 테이블 구조와 인덱스, Redis TTL과 무효화 정책, Cache stale 방어, Feed Session과 Pagination, 채널별 쿼리, 점수 항의 구체 수식과 계수, 다양성 조정, Cold Start, Feed 테스트가 여기에 해당합니다. ## 15. MVP 범위와 제외 범위 @@ -1005,6 +1061,7 @@ Feed의 상세 구현은 back/docs에서 관리합니다. `feed_event`, IMPRESSI - 검색어 LLM 분해 - 학습형 Feed Ranking - Multi-Armed Bandit +- Place category·region 기반 Feed 후보 채널과 점수 가중치(14.2) - H200 실시간 추론 - 자동 Collection Keyword 물리 집계 @@ -1071,16 +1128,17 @@ Context 수정 경합은 삭제 경합과 동일한 방어를 사용하므로, | 문서 | 내용 | |---|---| -| `ai/ai-integration.md` | FastAPI Client와 호출 시점 | -| `ai/context-state-sync.md` | Context 생성·삭제 동기화와 그 조합인 수정 | -| `ai/ai-rescan-scheduler.md` | 재스캔 Scheduler와 FAILED Finalizer | -| `ai/deletion-cancellation.md` | 삭제와 취소 처리 | -| `ai/ai-response-assembly.md` | Keyword Visibility 응답 조립 | -| `feed/feed-recommendation.md` | 추천 파이프라인 | -| `feed/feed-event.md` | 이벤트 수집 | -| `feed/feed-profile-cache.md` | Redis Cache | -| `feed/feed-scoring.md` | 점수 계산 | -| `feed/feed-tests.md` | Feed 테스트 | +| `ai/spec/ai-integration.md` | FastAPI Client와 호출 시점 | +| `ai/spec/context-state-sync.md` | Context 생성·삭제 동기화와 그 조합인 수정 | +| `ai/spec/ai-rescan-scheduler.md` | 재스캔 Scheduler와 FAILED Finalizer | +| `ai/spec/deletion-cancellation.md` | 삭제와 취소 처리 | +| `ai/spec/ai-response-assembly.md` | Keyword Visibility 응답 조립 | +| `ai/spec/feed-recommendation.md` | 추천 파이프라인 (14.2·14.5 구현) | +| `ai/spec/feed-scoring.md` | 후보 채널 쿼리와 점수 계산 (14.2·14.3 구현) | +| `ai/spec/feed-event.md` | `core.feed_event`와 이벤트 수집 (14.6 구현) | +| `ai/spec/feed-profile-cache.md` | Redis Cache와 stale 방어 | +| `ai/spec/feed-tests.md` | Feed 테스트 | +| `ai/proposals/P42-feed-mvp-without-place-metadata.md` | MVP Feed에서 Place category·region을 제외한 결정 근거 | ### 관련 공용 문서 diff --git "a/static/08_API_\353\252\205\354\204\270.md" "b/static/08_API_\353\252\205\354\204\270.md" index c24a670..0fa846f 100644 --- "a/static/08_API_\353\252\205\354\204\270.md" +++ "b/static/08_API_\353\252\205\354\204\270.md" @@ -932,11 +932,13 @@ DELETE /api/core/v1/collections/{collectionId}/records/{recordId} ## 8.1 최초 공개 책장 탐색 ```http -GET /api/core/v1/feed/collections/{collectionId}/shelf?cursor={cursor}&size=10 +GET /api/core/v1/feed/collections/{collectionId}/shelf?cursor={cursor}&size=20 ``` `collectionId`를 공개 진입점으로 사용해 해당 Collection 작성자의 다른 공개 Collection을 조회한다. +`size`는 공통 커서 계약을 따른다 — 기본값 `CursorPage.DEFAULT_SIZE`(20), 서버 방어 상한 `CursorPage.MAX_SIZE`(100), 범위 밖 값은 `CursorPage.normalizeSize`가 보정한다. 같은 Feed 네임스페이스의 `GET /feed/collections`와 기본 크기를 맞춘다. + 응답: ```json @@ -1156,12 +1158,16 @@ GET /api/core/v1/feed/collections?cursor={cursor}&size=20 규칙: -- 공개 가능한 Keyword만 반환 -- AI 처리가 끝나지 않았다면 `keywords: []` -- AI 미완료 Collection도 Feed 후보에 포함 가능 -- Context 원문과 사용자 신원은 반환하지 않음 +- `size` 기본 20. 커서는 공통 페이지네이션 계약(1.4)을 따르며 `cursor`는 opaque 문자열이다. +- `requestId`는 Feed Session 식별자다. 같은 Session의 다음 페이지는 `nextCursor`로 이어받고, 클라이언트는 이 값을 10.2의 이벤트 요청에 그대로 돌려보낸다. +- 공개 가능한 `PUBLIC` Keyword만 반환한다. `PRIVATE_ONLY`·`BLOCKED`는 타인 노출과 타인 Collection 특징 계산 모두에서 제외한다. +- AI 처리가 끝나지 않았다면 `keywords: []`다. 오류가 아니다. +- AI 미완료 Collection도 Feed 후보에 포함한다. +- Context 원문과 사용자 신원은 반환하지 않는다. **소유자 식별자(`memberId` 등)를 응답에 넣지 않는다.** - 상세 조회는 IMPRESSION 기록 대상이 아님 +후보 채널·점수 구성·가중치 등 추천 정책은 [AI 설계](05_AI_설계.md) 14장이 정본이다. + Collection 선택: ```http @@ -1176,27 +1182,15 @@ GET /api/core/v1/collections/{collectionId} POST /api/core/v1/feed/events ``` -CLICK: +`requestId`는 배열 바깥의 별도 필드다. 10.1 응답에서 받은 값을 그대로 돌려보낸다. 이벤트는 배열로 묶어 한 번에 보낸다. ```json { - "event": "CLICK", - "collectionId": 7001, - "placeId": null, "requestId": "5b2c0000-0000-0000-0000-000000000000", - "position": 0 -} -``` - -SAVE: - -```json -{ - "event": "SAVE", - "collectionId": 7001, - "placeId": 5501, - "requestId": "5b2c0000-0000-0000-0000-000000000000", - "position": 0 + "events": [ + { "event": "CLICK", "collectionId": 7001, "placeId": null, "position": 0 }, + { "event": "SAVE", "collectionId": 7001, "placeId": 5501, "position": 0 } + ] } ``` @@ -1206,7 +1200,15 @@ SAVE: 204 No Content ``` -IMPRESSION은 클라이언트가 보내지 않는다. +규칙: + +- `events` 배열의 크기 상한은 **100개**다. 초과하면 `400 INVALID_INPUT`이다. `recordIds` 배열과 같은 값이며 근거는 [파트간 요구사항](05-1_파트간_요구사항.md) 1.5에 있다. +- `event`는 `CLICK`·`SAVE`만 허용한다. **IMPRESSION은 서버가 10.1 응답 생성 시 기록하므로 클라이언트가 보내면 `400`으로 거부한다.** +- 사용자 식별자는 본문으로 받지 않는다. 인증 컨텍스트에서 가져온다. +- `placeId`는 Collection 안의 특정 Place를 대상으로 한 경우에만 채우고, 아니면 `null`이다. +- `position`은 10.1 응답에서 받은 값을 그대로 돌려보낸다. +- 이벤트는 관측 로그이므로 개별 항목이 유효하지 않으면(예: 삭제된 Collection) 그 항목만 버리고 나머지는 저장한다. 부분 실패로 전체를 실패시키지 않는다. +- 쓰기 전용이며 어떤 조회 결과도 반환하지 않는다. --- diff --git "a/static/10_MVP_\352\270\260\353\212\245\353\262\224\354\234\204.md" "b/static/10_MVP_\352\270\260\353\212\245\353\262\224\354\234\204.md" index ec71c3b..bee8a45 100644 --- "a/static/10_MVP_\352\270\260\353\212\245\353\262\224\354\234\204.md" +++ "b/static/10_MVP_\352\270\260\353\212\245\353\262\224\354\234\204.md" @@ -63,7 +63,8 @@ ### Feed와 공개 -- Spring 규칙 기반 발행 Collection 추천·상세 조회 +- Spring 규칙 기반 발행 Collection 추천·상세 조회 — 후보는 최신 발행·팔로우·무작위 세 채널 +- 팔로우·공개 Keyword·최신성·노출 패널티 기반 점수 계산 - Redis 기반 Feed Cache와 노출·클릭·저장 이벤트 수집 - 같은 Shelf의 다른 Collection 조회 - Feed에서 Shelf Follow @@ -99,3 +100,4 @@ - 검색어 LLM 분해 - 학습형 Feed Ranking과 Multi-Armed Bandit - 자동 Collection Keyword 물리 집계 +- Place category·region 기반 Feed 후보 채널과 점수 가중치([AI 설계](05_AI_설계.md) 14.2)