---
name: instagram-dm
description: "인스타그램 DM 대화방 하나를 처음부터 끝까지 읽어, 시각이 붙은 메시지 목록 thread.json과 읽기 쉬운 transcript.md로 저장한다. 인스타 DM 대화를 통째로 읽거나 저장하거나 요약하고 분석해 달라는 요청, instagram.com/direct/t/ 대화방 주소나 대화방 id를 받았을 때 쓴다. 로그인된 Chrome을 BrowserSkill로 스크롤하며 읽기만 하고, 아무것도 누르거나 보내지 않는다."
---

# 인스타그램 DM 읽기

인스타그램 DM 대화방 하나를 `thread.json`과 `transcript.md`로 바꾼다. 페이지를 넘기며 읽는 일은 스크립트가 하고, 뜻을 읽는 일은 그 결과를 보고 한다. 대화 목록은 화면 근처의 줄만 그려 두기 때문에, 화면을 캡처하거나 손으로 넘겨 가며 읽으면 느리고 줄이 빠진다.

## 준비

- node 22 이상과 PATH의 `bsk`(BrowserSkill CLI).
- 인스타그램에 로그인된 Chrome. 브라우저가 여럿 연결되어 있으면 `bsk browsers`로 그 Chrome의 id를 보고 `--browser`로 넘긴다.
- 인스타그램 웹의 화면 언어가 한국어여야 한다. 보낸 사람과 시각을 한국어 표시에서 읽는다.

## 실행

`node scripts/read.mjs <대화방 주소 또는 id> --out <폴더> [--me <아이디>] [--browser <id>]`

대화방 id는 주소 `https://www.instagram.com/direct/t/<id>/`의 숫자다. 스크립트는 1200x3000 창의 BrowserSkill 세션을 열어 대화방을 맨 위까지 불러온 뒤 위에서부터 줄마다 읽고, 끝나면 세션을 닫는다. 스크롤하고 읽기만 하니 메시지가 나갈 일이 없다. 430줄짜리 대화가 1분 안에 끝났다.

`--out` 폴더에는 읽은 줄 그대로인 `raw.json`과 `thread.json`, `transcript.md`를 쓴다. 표준 출력에는 맨 위에 닿았는지, 몇 줄을 읽었는지, 보낸 사람별 메시지 수, 첫 메시지와 마지막 메시지의 시각이 나온다.

`node scripts/read.mjs --raw <raw.json> --out <폴더> [--me <아이디>]`는 저장해 둔 `raw.json`을 브라우저 없이 다시 정리한다. 정리 규칙을 고쳤거나 `--me`를 바꿀 때 쓴다.

`--out -`이면 파일을 하나도 쓰지 않고(`raw.json`도 남지 않는다) `thread.json`을 표준 출력에, 요약을 표준 오류에 낸다. 파일을 쓸 수 없는 곳에서 쓰고, 그 출력은 표준 입력을 읽는 도구에 파이프로 넘긴다.

종료 코드는 셋이다.

- 0: 대화방 맨 위부터 모든 줄을 읽었다.
- 1: 파일은 썼지만 맨 위에 닿지 못했거나(`top: NOT REACHED`) 읽지 못한 줄이 남았다(`UNREAD`).
- 2: 인자가 틀렸거나, `bsk`를 못 찾았거나, 대화방이 열리지 않았거나, 화면이 이 스크립트가 아는 꼴이 아니다. 무엇이 틀렸는지는 표준 오류에 나온다. 다 읽은 뒤 정리하다 멈췄으면 `raw.json`은 남아 있다.

## 결과를 보고 정할 것

- 맨 위에 닿았는지는 출력의 `above the first message`로 확인한다. 대화방 맨 위에는 상대의 프로필 칸(이름, `<아이디> · Instagram`, 프로필 보기)이 있으니, 이 글자가 아니면 맨 위가 아니다.
- 종료 코드가 1이면 한 번 더 돌린다. 그래도 1이면 `raw.json`의 `missing`(읽지 못한 줄의 번호, 가장 최근 줄이 0)을 보고 빠진 곳이 있다고 사용자에게 말한다. `empty`에 든 줄은 비어 있는 칸이라 빠진 메시지가 아니다.
- `me`가 null이면 대화의 어느 쪽이 사용자인지 모르는 것이다. 단체방이거나 상대의 프로필 표시가 없을 때 그렇다. 사용자에게 아이디를 물어 `--raw`와 `--me`로 다시 정리하고, 추측해서 정하지 않는다.
- `UNKNOWN_HEADER`는 처음 보는 시각 표시다. `scripts/lib/rows.mjs`의 `parseHeader`에 그 꼴과 테스트를 더한 뒤 `--raw`로 다시 정리한다.
- `UI_LANGUAGE`면 인스타그램 화면 언어를 한국어로 바꾸고 다시 읽는다. `NOT_THREAD`면 주소와 로그인을 확인하고, `MOVED`는 읽는 사이 새 메시지가 온 것이니 다시 돌린다. `LAYOUT`은 인스타그램 화면 구조가 바뀐 것이라 `read.mjs`의 읽는 법을 고쳐야 한다.

## 결과

`thread.json`(UTF-8, 들여쓰기 2칸):

- `source` "instagram", `title`(상대 아이디), `me`(사용자 아이디 또는 null), `participants`(첫 메시지 순서의 아이디 목록), `timePrecision` "block", `messages`(오래된 것부터).
- 메시지: `at` 현지 시각 `YYYY-MM-DDTHH:MM`(시간대 없음), `who`(아이디. 누가 했는지 화면에 없는 줄은 null), `kind`("text", "media", "call", "story", "system"), `text`(화면의 버튼 글자를 뺀 내용, 없으면 빈 문자열).
- 해당할 때만 붙는 키: `media` `{photos, videos, audio, once}`(개수, 한 번 보기면 `once` true), `call` `{type: "voice"|"video", event: "start"|"end"|"missed"}`, `story` "reply"|"like", `replyTo`(답장한 메시지의 글), `reactions`(공감 이모지), `notes`(메시지 아래 글자로 붙은 표시), `edited` true, `links`(메시지에 보이는 주소).
- 통화 시작에 이름이 붙어 있으면 그 사람이 건 것이고, 이름이 없으면 사용자가 건 것이다. 통화 종료와 부재중은 누구 쪽인지 화면에 없어서 `who`가 null이다.
- 인스타그램은 메시지마다 시각을 보여 주지 않고, 대화가 끊겼다 이어진 자리에만 시각 표시를 넣는다. 그래서 메시지는 바로 위 시각 표시의 시각을 받고(`timePrecision` "block"), 같은 묶음 안의 답장 간격은 알 수 없다. 최근 한 주는 요일로만 보이므로 읽은 날 이전의 가장 가까운 그 요일로 정한다.

`transcript.md`: 제목, 기간과 보낸 사람별 개수 한 줄, 시각에 대한 한 줄, 날짜 제목(`## 2026-10-03 (토)`) 아래 `- HH:MM 아이디: 내용`. 내용 앞에는 `[답장: …]`, `[스토리 답장]` 또는 `[스토리 공감]`, `[사진 N장]`, `[동영상 N개]`, `[음성 메시지]`, `[한 번 보기]`가, 뒤에는 `(수정됨)`과 `[공감 ❤️]`가 붙는다.

## 화면이 보여 주지 않는 것

사진, 동영상, 음성 메시지의 내용(한 번 보기는 웹에서 열리지도 않는다), 보내기를 취소한 메시지, 읽은 시각, 같은 묶음 안의 메시지 간격.

## 개인 정보

대화는 상대의 개인 정보다. `--out`은 커밋하지 않는 폴더(저장소 안이라면 `.gitignore`에 든 곳)로 둔다. 표준 출력에는 아이디가 나온다.
