근거: docs/reference/04_익명SNS_공개정책.md, docs/reference/06_데이터모델_및_무결성.md(5장 공개 범위), docs/reference/08_API_명세.md(7·12·14장).
PinLog은 익명 SNS다. 타인에게는 발행된 Collection과 공개 가능한 정보만 보인다. 아래는 프론트가 지켜야 할 최소 규칙이며, 위반은 즉시 개인정보·신원 노출 사고다.
- 타인 조회 응답에서 Record의
contexts는 **항상null**이다(08_API_명세14.1, DTORecordDetail.contexts: ContextDetail[] | null). - 공개 진입 경로는 Feed / 타인 Shelf / Collection 상세 셋뿐이다. 이 화면에서 Context 원문을 렌더링하지 않는다.
ownedByMe(Collection 상세) 또는contexts === null여부로 소유자/타인을 구분한다. 타인이면 원문 영역 자체를 그리지 않는다.- Context 원문이 반드시 사용자가 직접 쓴 메모인 것은 아니다. 대화 캡처 이미지로 기록하는 경로(front#114,
POST /places/suggestions)에서는 AI가 추출한extracted.contextSuggestion이 본문 초안으로 채워진다 — 사용자가 그대로 저장하면 제3자의 발화에서 파생된 문장이 Context 원문이 된다. 규칙 자체는 그대로지만 위반 시 대가가 커졌으니, 이 화면 계열을 손댈 때 위 세 줄을 특히 엄격히 본다.- 같은 응답의
extracted.evidence(대화 근거 문장)는 저장·전송하지 않는다. 현재는 기록 시트 안 읽기 전용 표시에만 쓰인다(front#114 확인). 이 값을 Context 본문·Collection·검색 색인 등 서버로 나가는 어떤 필드에도 넣지 않는다.
- 같은 응답의
- 내부 사용자 ID는 순차값이라 노출 시 사용자 열거·가입 순번 추론이 가능하다. URL·요청·응답 어디에도 쓰지 않는다. 예외 없음 — 로그인 등 본인 응답에도
memberId를 포함하지 않는다(docs/reference/08_API_명세.md1.1,docs/reference/11_인증_설계.md4.6).과거엔 "로그인·가입 확정 응답의 본인 memberId만 예외로 수신"이었으나, 쿠키 기반 인증 확정으로 폐기됨(2026-07-27) — 서버가 세션을 쿠키로 식별하므로 프론트가 자신의 memberId를 알 필요가 없어졌다.
- 타인 접근의 진입점은 Collection id다. 팔로우·책장 탐색은 Collection id로 시작한다.
POST /follows { collectionId },GET /feed/collections/{collectionId}/shelf,GET /follows/{followId}/collections
- 모든 Keyword 응답은
keyword_preset의display_name(표시 문자열, 구 컬럼명label) 배열이다.code는 내부 식별용이며 노출·전송하지 않는다(08_API_명세6.1). - Keyword는 사전 정의 프리셋에서만 온다. 프론트가 임의 Keyword를 만들거나 편집하지 않는다.
| 등급 | 본인 | 타인 |
|---|---|---|
PUBLIC |
제공 | 제공(공개 Collection 안에서) |
PRIVATE_ONLY |
제공 | 비공개(서버가 필터) |
BLOCKED |
비공개 | 비공개 |
- 타인 화면에는 서버가
PUBLIC만 내려준다. 프론트가 등급으로 필터링을 재구현하지 않는다(서버 계약을 신뢰하되, 타인 화면에 등급 원본을 노출하지 않는다). - 본인 화면에서
PRIVATE_ONLY를 시각적으로 구분 표시할지는 제품 미결정이다(05-1_파트간_요구사항1.4).docs/api-contract.md의 "협의 필요"를 확인하고, 확정 전까지 구분 UI를 임의로 만들지 않는다.
Context 원문, 사용자 내부 ID, 실명, 닉네임, 소셜 계정, 팔로워·팔로잉 목록, 다른 팔로워가 설정한 별칭.
- Follow 별칭(
alias)은 지정한 본인에게만 보인다. 타인 화면에 노출하지 않는다.
// ❌ 타인 응답의 contexts는 null이다. 아래는 런타임 크래시이자 노출 시도다.
function RecordCard({ record }: { record: RecordDetail }) {
return <p>{record.contexts[0].body}</p>;
}// ✅ 타인이면 원문 영역을 그리지 않고 공개 정보(Place, Keyword, 생성일)만 표시
function RecordCard({ record }: { record: RecordDetail }) {
const isOwner = record.contexts !== null;
return (
<article>
<PlaceInfo place={record.place} />
<KeywordList displayNames={record.keywords} /> {/* display_name 문자열만 */}
{isOwner && <ContextList contexts={record.contexts!} />}
</article>
);
}// ❌ member.id를 경로에 노출
navigate(`/shelf/${authorMemberId}`);// ✅ 발행된 Collection id로 책장/팔로우 진입
navigate(`/collections/${collectionId}`);
followMutation.mutate({ collectionId });// ❌ code는 내부 식별용. 응답에 없고, 있어도 노출하지 않는다.
sendEvent({ keywordCode: kw.code });// ✅ 화면·전송 모두 display_name 문자열만 사용
{
record.keywords.map((displayName) => <Chip key={displayName}>{displayName}</Chip>);
}