Skip to content

Latest commit

 

History

History
100 lines (73 loc) · 5.69 KB

File metadata and controls

100 lines (73 loc) · 5.69 KB

공개 범위 규칙

근거: docs/reference/04_익명SNS_공개정책.md, docs/reference/06_데이터모델_및_무결성.md(5장 공개 범위), docs/reference/08_API_명세.md(7·12·14장).

PinLog은 익명 SNS다. 타인에게는 발행된 Collection과 공개 가능한 정보만 보인다. 아래는 프론트가 지켜야 할 최소 규칙이며, 위반은 즉시 개인정보·신원 노출 사고다.

1. Context 원문은 타인에게 노출하지 않는다

  • 타인 조회 응답에서 Record의 contexts는 **항상 null**이다(08_API_명세 14.1, DTO RecordDetail.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·검색 색인 등 서버로 나가는 어떤 필드에도 넣지 않는다.

2. member.id(내부 사용자 ID)를 쓰지 않는다

  • 내부 사용자 ID는 순차값이라 노출 시 사용자 열거·가입 순번 추론이 가능하다. URL·요청·응답 어디에도 쓰지 않는다. 예외 없음 — 로그인 등 본인 응답에도 memberId를 포함하지 않는다(docs/reference/08_API_명세.md 1.1, docs/reference/11_인증_설계.md 4.6).
    • 과거엔 "로그인·가입 확정 응답의 본인 memberId만 예외로 수신"이었으나, 쿠키 기반 인증 확정으로 폐기됨(2026-07-27) — 서버가 세션을 쿠키로 식별하므로 프론트가 자신의 memberId를 알 필요가 없어졌다.
  • 타인 접근의 진입점은 Collection id다. 팔로우·책장 탐색은 Collection id로 시작한다.
    • POST /follows { collectionId }, GET /feed/collections/{collectionId}/shelf, GET /follows/{followId}/collections

3. Keyword는 display_name만, code는 노출하지 않는다

  • 모든 Keyword 응답은 keyword_presetdisplay_name(표시 문자열, 구 컬럼명 label) 배열이다. code는 내부 식별용이며 노출·전송하지 않는다(08_API_명세 6.1).
  • Keyword는 사전 정의 프리셋에서만 온다. 프론트가 임의 Keyword를 만들거나 편집하지 않는다.

4. Keyword 공개 등급 (참조: 03_공식_용어사전.md, 04_익명SNS_공개정책.md)

등급 본인 타인
PUBLIC 제공 제공(공개 Collection 안에서)
PRIVATE_ONLY 제공 비공개(서버가 필터)
BLOCKED 비공개 비공개
  • 타인 화면에는 서버가 PUBLIC만 내려준다. 프론트가 등급으로 필터링을 재구현하지 않는다(서버 계약을 신뢰하되, 타인 화면에 등급 원본을 노출하지 않는다).
  • 본인 화면에서 PRIVATE_ONLY시각적으로 구분 표시할지는 제품 미결정이다(05-1_파트간_요구사항 1.4). docs/api-contract.md의 "협의 필요"를 확인하고, 확정 전까지 구분 UI를 임의로 만들지 않는다.

5. 공개 조회에서 금지되는 정보 (08_API_명세 7.3)

Context 원문, 사용자 내부 ID, 실명, 닉네임, 소셜 계정, 팔로워·팔로잉 목록, 다른 팔로워가 설정한 별칭.

  • Follow 별칭(alias)은 지정한 본인에게만 보인다. 타인 화면에 노출하지 않는다.

6. 예시

잘못된 예 — 타인 Collection에서 Context 원문 렌더

// ❌ 타인 응답의 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>
  );
}

잘못된 예 — 내부 ID를 URL에 사용

// ❌ member.id를 경로에 노출
navigate(`/shelf/${authorMemberId}`);

올바른 예 — Collection id를 진입점으로

// ✅ 발행된 Collection id로 책장/팔로우 진입
navigate(`/collections/${collectionId}`);
followMutation.mutate({ collectionId });

잘못된 예 — Keyword code 전송·노출

// ❌ code는 내부 식별용. 응답에 없고, 있어도 노출하지 않는다.
sendEvent({ keywordCode: kw.code });

올바른 예 — display_name만 사용

// ✅ 화면·전송 모두 display_name 문자열만 사용
{
  record.keywords.map((displayName) => <Chip key={displayName}>{displayName}</Chip>);
}