Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions static/02_정책_정의서.md
Original file line number Diff line number Diff line change
Expand Up @@ -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를 소프트 삭제합니다.
Expand Down
2 changes: 2 additions & 0 deletions static/04_익명SNS_공개정책.md
Original file line number Diff line number Diff line change
Expand Up @@ -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에 포함하지 않습니다.
Expand Down
12 changes: 8 additions & 4 deletions static/05-1_파트간_요구사항.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 공개 등급 표시 (확인 필요)

Expand All @@ -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에 있습니다.
Expand Down
106 changes: 82 additions & 24 deletions static/05_AI_설계.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 목적

Expand Down Expand Up @@ -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. 시스템 구성과 책임
Expand Down Expand Up @@ -939,39 +939,95 @@ 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 요청 처리 중 다음을 호출하지 않습니다.

- FastAPI의 어떤 API
- 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 범위와 제외 범위

Expand Down Expand Up @@ -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 물리 집계

Expand Down Expand Up @@ -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을 제외한 결정 근거 |

### 관련 공용 문서

Expand Down
48 changes: 25 additions & 23 deletions static/08_API_명세.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 }
]
}
```

Expand All @@ -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) 그 항목만 버리고 나머지는 저장한다. 부분 실패로 전체를 실패시키지 않는다.
- 쓰기 전용이며 어떤 조회 결과도 반환하지 않는다.

---

Expand Down
4 changes: 3 additions & 1 deletion static/10_MVP_기능범위.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@

### Feed와 공개

- Spring 규칙 기반 발행 Collection 추천·상세 조회
- Spring 규칙 기반 발행 Collection 추천·상세 조회 — 후보는 최신 발행·팔로우·무작위 세 채널
- 팔로우·공개 Keyword·최신성·노출 패널티 기반 점수 계산
- Redis 기반 Feed Cache와 노출·클릭·저장 이벤트 수집
- 같은 Shelf의 다른 Collection 조회
- Feed에서 Shelf Follow
Expand Down Expand Up @@ -99,3 +100,4 @@
- 검색어 LLM 분해
- 학습형 Feed Ranking과 Multi-Armed Bandit
- 자동 Collection Keyword 물리 집계
- Place category·region 기반 Feed 후보 채널과 점수 가중치([AI 설계](05_AI_설계.md) 14.2)