독서 계획 및 페이지 속도 플래너
Wikiprompt, 무료 프롬프트 백과사전에서
독서 계획 및 페이지 속도 플래너 독서 클럽, 강좌, 또는 시험 마감일을 맞추기 위해 페이지 수, 단어 수, 읽기 속도, 그리고 가용 시간을 활용하여 일일 독서 일정을 계획하는 종합적인 AI 스킬입니다. 이 스킬에는 전체 시스템 프롬프트, 워크플로우, 참조 가이드, 템플릿, 예시, 그리고 Python 플래너 스크립트가 포함되어 있습니다.
프롬프트 내용저장
🌐
---
name: reading-plan-and-page-pace-planner
description: 책을 하루하루 계획해서 북클럽, 강의 또는 시험 날짜 전에 모든 책을 끝낼 수 있게 도와줍니다 - 페이지 수나 단어 수(또는 오디오북 길이), 독서 속도, 평일마다 가용한 시간, 여행과 바쁜 날짜를 기준으로 합니다. 페이지를 고르게 분배하고, 각 마감일 전에 버퍼 일수를 유지하며, 주간 보충일을 두고, 챕터 끝에서 멈추며, 마감일이 도달 불가능할 때 경고하고 현재 위치에서 다시 계획합니다. 표준 라이브러리 파이썬 플래너를 포함합니다.
---
# 독서 계획 및 페이지 속도 플래너
북클럽 전날 밤에 200페이지를 벼락치기하는 독자들을 돕습니다. 사람들은 대략적인 시간(평일 30분, 주말에는 더 많음)을 알지만, 2주 후 마감인 245페이지 소설, 어려운 업무용 책, 같은 달에 읽어야 할 880페이지 고전이 페이지 수로 얼마나 되는지는 모릅니다. 책, 독서 속도, 자유 시간을 구체적인 일일 계획("화: p. 1-27")으로 바꾸고, 현실적인 여유를 유지하며, 마감일을 맞출 수 없을 때와 해결 방법을 조기에 명확하게 알려줍니다.
## 이 스킬의 파일들
- `scripts/reading_plan.py` - 계획 JSON(페이지, 단어 또는 오디오북 시간이 있는 책, 시작 페이지, 난이도, 마감일, 챕터 시작점; 분당 단어 수 또는 시간당 페이지 수의 독서 속도; 요일별 분; 여행 및 예외; 보충 요일; 버퍼 일수)을 읽습니다; 가장 이른 마감일부터 계획하고, 부하를 고르게(또는 가능한 한 빨리) 분산하며, 일일 중단점을 챕터 끝에 맞추고, 책 요약, 발견 사항 RP01-RP06, 페이지 범위가 있는 일일 표, 주간 체크포인트 및 보충 조언을 출력합니다; `--from` + `--at "Title=page"`는 중간에 다시 계획합니다(Python 3.8+ 표준 라이브러리만 사용)
- `references/reading-speed-and-estimates.md` - 속도 측정, 책 유형별 페이지당 단어 수, 난이도 요소, 오디오북
- `references/pacing-and-catch-up.md` - 버퍼, 보충일, 뒤처졌을 때 또는 마감일이 불가능할 때 해야 할 일
- `templates/reading-plan.md` - 전달할 계획(캘린더 친화적)
- `examples/book-club.json` - 11월-12월: 목요일 북클럽용 소설, 이미 p. 23에서 시작한 업무용 책, 고전 클럽용 Middlemarch, 마감일이 없는 오디오북, 일요일 보충일, 주말 여행 및 특이한 이틀
- `examples/book-club-plan.md` - 해당 파일에 대한 실제 플래너 출력
## 워크플로우
### 1. 입력 수집
다음을 요청합니다: 각 책의 페이지 수(또는 단어 수; 판본이 중요함), 현재 위치, 마감일과 그 성격(모임, 시험), 쉬운지 어려운지; 독서 속도(또는 `references/reading-speed-and-estimates.md`로 10분 테스트 시간 측정); 이상적인 시간이 아닌 현실적인 평일 분; 알려진 여행, 늦은 교대, 자유일; 보충일로 남겨둘 요일; 각 모임 전에 끝내고 싶은 일수(버퍼, 기본 1-2일).
### 2. 플래너 실행
```
python3 scripts/reading_plan.py examples/book-club.json
python3 scripts/reading_plan.py examples/book-club.json --mode asap
python3 scripts/reading_plan.py examples/book-club.json --from 2026-11-20 --at "Thinking in Systems=110"
python3 scripts/reading_plan.py plan.json --json
python3 scripts/reading_plan.py --self-test
```
종료 0 = 모든 마감일 충족, 1 = 최소 한 권의 책을 제시간에 끝낼 수 없음, 2 = 잘못된 입력. 셸이 없는 경우: 필요 분 = 페이지 x 페이지당 단어 수 / 분당 단어 수 x 난이도; 각 완료 날짜(마감일에서 버퍼를 뺀 값, 보충일 제외)까지 사용 가능한 분과 비교, 가장 이른 마감일 우선.
### 3. 결과 읽기
- RP01(높음): 책이 마감일 전에 맞지 않습니다. 메시지는 부족한 페이지 수와 이를 해결할 독서일당 추가 분을 제공합니다. 계획이 불가능할 때 플래너는 그 전에 전체 속도로 읽으므로 이전 책이 일찍 끝나 시간을 확보합니다.
- RP02 / RP04(중간): 계획이 버퍼를 사용해야만 작동하거나, 가장 바쁜 구간에서 시간의 90% 이상을 사용합니다 - 한 번의 나쁜 주가 계획을 깨뜨립니다.
- RP03(낮음): 최대 세션보다 긴 날 - 분할하세요. RP05/RP06(정보): 완료된 책, 촉박한 마감일, 오디오북.
- 일일 표에서 무거운 주말 날짜와 주간 체크포인트에서 "일요일까지 어디쯤 있어야 하는지"를 확인하세요.
### 4. 불가능한 계획 수정
다음 순서로 옵션을 제안하세요: 긴 책을 지금 병렬로 시작(마감일 로직을 더 일찍 이동), 일부 날짜에 10-20분 추가, 보충일 사용, 일부를 통근용 오디오북으로 전환, 명확히 표시된 섹션 훑어보기 또는 건너뛰기, 또는 모임에 중간쯤 도착하는 것을 수용하고 그렇게 말하기. 각 변경 후 다시 실행하세요. `references/pacing-and-catch-up.md` 참조.
### 5. 전달
`templates/reading-plan.md` 작성: 책별 요약, 주별 계획(날짜 및 페이지 범위, 캘린더나 책갈피 메모에 적합), 보충 규칙, 재계획 명령. 독자가 현재 페이지를 보고할 때마다 빠른 재계획을 제공하세요.
## 예시 결과
`examples/book-club.json`(240 wpm, 하루 20-60분, 일요일 보충용 자유, 버퍼 2일)에서 *The Remains of the Day*는 11월 19일 북클럽을 위해 11월 12일에 챕터 끝에서 완료되고, *Thinking in Systems*(어려움, 계수 1.3, p. 23부터)는 12월 1일 그룹을 위해 11월 27일에 완료됩니다. *Middlemarch*(316,000단어, 약 22시간)는 12월 17일 고전 클럽까지 끝낼 수 없습니다: 약 452페이지 부족, 예상 완료 1월 11일(RP01, 높음). 해결책: 42일의 독서일 각각에 약 +17분, 또는 지금 다른 두 권과 병렬로 시작. 120분의 조용한 토요일은 두 세션으로 표시되고(RP03), Piranesi 오디오북(1.25배속으로 6시간 40분)은 Middlemarch 이후에 이어집니다.
## 규칙
- 사람들이 실제로 가진 시간으로 계획하고, 모든 모임 전에 최소한 하루의 버퍼를 유지하세요.
- 조용히 날짜를 과도하게 채우지 마세요: 맞지 않으면 어떤 마감일이 몇 페이지 부족으로 실패하는지, 무엇이 해결할지 말하세요.
- 판본의 페이지 수를 사용하세요; 단어 수만 알면 페이지가 추정치라고 말하세요.
- 계획에서 보충일은 비워두세요 - 뒤처졌을 때를 위한 것이지, 더 읽기 위한 것이 아닙니다.
- 독서는 즐거워야 합니다: 매일 2시간 세션을 제안하기 전에 건너뛰기나 형식 전환을 제안하세요.
- 독자가 뒤처지거나 앞서 있을 때마다 현재 페이지에서 다시 계획하세요; 오래된 일정에 "따라잡도록" 강요하지 마세요.
FILE:references/reading-speed-and-estimates.md
# 독서 속도 및 추정
## 속도 측정(10분)
1. 실제 책(또는 비슷한 책)을 선택하고, 챕터 시작에서 시작하여 10분 타이머를 설정하고 평소처럼 읽으세요.
2. 세 개의 전체 줄의 단어 수를 세고, 3으로 나누면 줄당 단어 수; 전체 페이지의 줄 수를 세면 페이지당 줄 수.
3. 페이지당 단어 수 = 줄당 단어 수 x 페이지당 줄 수; 분당 단어 수 = 읽은 페이지 x 페이지당 단어 수 / 10.
또는 몇 페이지의 시간을 재고 분당 단어 수 대신 `"pages_per_hour"`를 사용하세요.
## 일반적인 값
| 독자 / 자료 | 분당 단어 수 |
|---|---|
| 주의 깊은 읽기, 어려운 논픽션, 제2언어 | 150-200 |
| 평균 성인, 소설 | 200-260 |
| 빠른 여가 독자 | 300-400 |
훑어보기는 읽기가 아닙니다: 450 이상의 속도로 계획하지 마세요.
| 책 유형 | 페이지당 단어 수 |
|---|---|
| 대중 시장 페이퍼백 | 300-350 |
| 트레이드 페이퍼백 소설 | 250-300 |
| 주석이 있는 하드커버 논픽션 | 350-450 |
| 큰 활자, 아동 챕터북 | 150-200 |
| 전자책 | 단어 수 사용(스토어 페이지 또는 표시 도구) 및 기본 275단어/페이지 |
## 난이도 계수
`"difficulty"`는 읽기 시간을 곱합니다: 1.0 쉬운 소설, 1.2-1.4 어려운 논픽션, 긴 문장의 고전, 다이어그램이 있는 기술 서적, 1.5+ 필기하는 교과서, 시, 제2언어 읽기.
## 페이지 수 vs 단어 수
- 독자가 가진 판본의 페이지 수를 사용하세요; 같은 책의 판본은 30%까지 다를 수 있습니다.
- `"pages"`와 `"words"`가 모두 있으면 플래너는 해당 판본의 페이지당 단어 수를 도출합니다(Middlemarch 316,000 / 880 = 359).
- 단어만 있으면 페이지는 추정치입니다(단어 / 페이지당 단어 수); 계획에서 그렇게 말하세요.
## 오디오북
- `"audio_minutes"`는 런타임, `"speed"`는 재생 속도: 필요 시간 = 런타임 / 속도(1.25배속에서 400분 = 320분).
- 오디오는 통근, 집안일, 산책에 맞습니다: 그 시간이 추가인지 이미 일일 분에 포함되었는지 말하세요.
- 위치는 오디오의 h:mm으로 표시됩니다.
FILE:references/pacing-and-catch-up.md
# 속도 조절 및 보충
## 플래너가 계획하는 방법
- 가장 이른 마감일 우선: 가장 가까운 완료 날짜(마감일에서 버퍼 일수 뺀 값)가 있는 책을 먼저 읽습니다; 마감일이 없는 책은 마지막에 읽습니다.
- 균등 모드(기본): 가장 촉박한 마감일까지의 각 구간에 대해 제시간에 끝내는 일일 분의 가장 작은 균등 몫을 사용하므로, 여유가 있을 때 30분 날이 20분 날이 될 수 있습니다. 그 마감일 후 다음 구간이 다시 계산됩니다.
- ASAP 모드: 매일 모든 분을 사용합니다; 책은 가능한 한 빨리 끝납니다.
- 마감일이 전체 속도로도 맞지 않으면, 그 전의 모든 날이 전체 속도로 계획되고 책은 RP01로 표시됩니다.
- 일일 중단점은 `chapter_snap_pages`(기본 8) 내에 챕터 끝이 있고 그날 페이지의 4분의 1일 때 챕터 끝으로 이동합니다.
## 버퍼 및 보충일
- 버퍼 일수(`buffer_days`, 기본 1)는 계획된 완료와 모임 사이에 있습니다: 노트를 다시 읽거나 나쁜 주를 흡수할 시간.
- 보충 요일(`catch_up_days`, 예: 일요일)은 계획된 읽기가 없습니다. 뒤처졌을 때만 사용하세요; 계획대로라면 자유 시간입니다.
- 예외(여행, 늦은 교대, 조용한 토요일)는 해당 날짜의 요일 분을 덮어씁니다.
## 뒤처졌을 때
1. 하루 놓침: 다음 보충일에 그날의 페이지를 읽거나, 다음 3일 각각에 3분의 1을 추가하세요.
2. 이틀 이상 뒤처짐: 오래된 계획을 쫓는 대신 `--from <today> --at "<Title>=<current page>"`로 오늘부터 다시 계획하세요.
3. 그래도 제시간에 안 됨(RP01): 하나 선택 - 긴 책을 지금 병렬로 시작, 며칠에 10-20분 추가, 통근에 오디오북 사용, 표시된 섹션 훑어보기(서문, 부록, 주석), 또는 중간쯤 도착하기로 결정하고 그룹에 말하기.
## 계획이 너무 빠듯한 신호
- RP04: 구간에서 시간의 90% 이상 사용; 어떤 질병이나 늦은 주도 계획을 깨뜨립니다.
- RP02: 책이 버퍼 일수를 사용해야만 맞습니다.
- "2 세션"으로 표시된 많은 날(RP03): 아침과 저녁에 의도적으로 읽기를 분산하는 것을 고려하세요.
## 북클럽 팁
- 그룹이 전체 책이 아닌 특정 페이지나 챕터를 논의할 경우 그 부분을 책의 `pages`로 사용하세요.
- 긴 고전의 경우, 모임이 전반부를 다루는지 클럽에 물어보고 두 개의 마감일을 계획하세요.
- 책에 계획의 주간 체크포인트가 있는 메모지를 붙이세요.
FILE:templates/reading-plan.md
# 독서 계획: <이름> (<시작>부터 <마지막 마감일>까지)
**속도:** <wpm> 단어/분, 30분 세션에서 약 <페이지> 페이지
**시간:** <요일별 분>; 보충일: <요일>; 버퍼: 각 모임 전 <n>일
**판정:** <모든 마감일 충족 | BOOK을 DATE까지 끝낼 수 없음 - 옵션 참조>
## 책
| 책 | 남은 페이지 | 필요 시간 | 모임 / 마감일 | 완료 날짜 | 계획 완료 |
|---|---|---|---|---|---|
| <제목> | <pp> | <h:mm> | <날짜 - 이벤트> | <날짜> | <날짜> |
## 주별
### <월요일 날짜> 주
- 월: <책 p. a-b>
- 화: <...>
- 일: 보충 / 자유
**일요일 밤 체크포인트:** <책 p. X>
## 마감일이 맞지 않는 경우
- 부족: <날짜>까지 <책>의 <페이지> 페이지
- 옵션: <독서일에 +N분 | 지금 병렬로 시작 | 통근용 오디오북 | x-y장 훑어보기 | 중간쯤 도착>
- 선택: <...>
## 뒤처졌을 때
- 하루 놓침? <보충일> 사용 또는 다음 3일에 <n>페이지 추가.
- 이틀 이상 뒤처짐: 오늘부터 다시 계획(`--from <날짜> --at "<제목>=<페이지>"`).
FILE:examples/book-club.json
{
"name": "독서 계획: 2026년 11월-12월 (두 북클럽과 업무용 책)",
"start": "2026-11-03",
"reader": {"words_per_minute": 240, "words_per_page": 280},
"minutes": {"mon": 30, "tue": 30, "wed": 20, "thu": 30, "fri": 25, "sat": 60, "sun": 45},
"catch_up_days": ["sun"],
"buffer_days": 2,
"max_session_minutes": 60,
"chapter_snap_pages": 8,
"exceptions": [
{"dates": "2026-11-13..2026-11-15", "minutes": 0, "note": "주말 여행"},
{"dates": "2026-11-26", "minutes": 15, "note": "늦은 교대"},
{"dates": "2026-12-05", "minutes": 120, "note": "조용한 토요일"}
],
"books": [
{"title": "The Remains of the Day", "pages": 245, "words_per_page": 265, "deadline": "2026-11-19", "event": "목요일 북클럽",
"chapters": [1, 9, 45, 101, 125, 165, 212, 239]},
{"title": "Thinking in Systems", "pages": 218, "start_page": 23, "difficulty": 1.3, "deadline": "2026-12-01", "event": "팀 독서 그룹"},
{"title": "Middlemarch", "words": 316000, "pages": 880, "deadline": "2026-12-17", "event": "고전 클럽"},
{"title": "Piranesi (오디오북)", "audio_minutes": 400, "speed": 1.25}
]
}
FILE:examples/book-club-plan.md
# 독서 계획: 2026년 11월-12월 (두 북클럽과 업무용 책)
시작 2026-11-03, 균등 모드, 각 마감일 전 2일 버퍼, 보충일: 일. 계획된 독서: 81일 동안 36시간 44분.
**판정: 1권의 책을 마감일까지 끝낼 수 없음 - 계획 변경**
## 책
| 책 | 남음 | 예상 시간 | 마감일 | 완료 날짜 | 계획 완료 | 여유 일수 | 독서일당 평균 |
|---|---|---|---|---|---|---|---|
| The Remains of the Day | 245 pp | 4:31 h | 2026-11-19 (목요일 북클럽) | 2026-11-17 | 2026-11-12 | 7 | 27 pp |
| Thinking in Systems | 196 pp | 4:57 h | 2026-12-01 (팀 독서 그룹) | 2026-11-29 | 2026-11-27 | 4 | 16 pp |
| Middlemarch | 880 pp | 21:57 h | 2026-12-17 (고전 클럽) | 2026-12-15 | 2027-01-11 | -25 | 23 pp |
| Piranesi (오디오북) | 6:40 오디오 | 5:20 h | - | - | 2027-01-22 | - | 0:36 |
## 발견 사항
- **높음 RP01** Middlemarch는 2026-12-17(고전 클럽)까지 끝낼 수 없음: 현재 속도로 약 452페이지 부족; 예상 완료 2027-01-11. 옵션: 그때까지 42일의 독서일 각각에 +17분, 더 빨리 읽기, 약 452페이지 훑어보기 또는 건너뛰기, 또는 더 일찍 시작
- **낮음 RP03** 60분 이상 계획된 1일(예: 2026-12-05: 120분) - 두 세션으로 분할
- **정보 RP06** Piranesi(오디오북)는 오디오북: 계획은 1.25배속 청취 시간을 계산합니다; 통근과 집안일로 충당 가능
## 일별
| 날짜 | 요일 | 분 | 읽을 내용 | 메모 |
|---|---|---|---|---|
| 2026-11-03 | 화 | 30 | The Remains of the Day p. 1-27 (27 pp) | |
| 2026-11-04 | 수 | 19 | The Remains of the Day p. 28-44 (17 pp) | |
| 2026-11-05 | 목 | 30 | The Remains of the Day p. 45-71 (27 pp) | |
| 2026-11-06 | 금 | 25 | The Remains of the Day p. 72-94 (23 pp) | |
| 2026-11-07 | 토 | 60 | The Remains of the Day p. 95-148 (54 pp) | |
| 2026-11-08 | 일 | - | - | 보충 / 여유 (45분 가능) |
| 2026-11-09 | 월 | 30 | The Remains of the Day p. 149-175 (27 pp) | |
| 2026-11-10 | 화 | 30 | The Remains of the Day p. 176-202 (27 pp) | |
| 2026-11-11 | 수 | 20 | The Remains of the Day p. 203-221 (19 pp) | |
| 2026-11-12 | 목 | 30 | The Remains of the Day p. 222-245 (24 pp); Thinking in Systems p. 23-24 (2 pp) | |
| 2026-11-13 | 금 | - | - | 주말 여행 |
| 2026-11-14 | 토 | - | - | 주말 여행 |
| 2026-11-15 | 일 | - | - | 주말 여행 |
| 2026-11-16 | 월 | 30 | Thinking in Systems p. 25-44 (20 pp) | |
| 2026-11-17 | 화 | 30 | Thinking in Systems p. 45-64 (20 pp) | The Remains of the Day 완료 날짜 |
| 2026-11-18 | 수 | 20 | Thinking in Systems p. 65-77 (13 pp) | |
| 2026-11-19 | 목 | 30 | Thinking in Systems p. 78-97 (20 pp) | 마감일 The Remains of the Day - 목요일 북클럽 |
| 2026-11-20 | 금 | 25 | Thinking in Systems p. 98-113 (16 pp) | |
| 2026-11-21 | 토 | 60 | Thinking in Systems p. 114-153 (40 pp) | |
| 2026-11-22 | 일 | - | - | 보충 / 여유 (45분 가능) |
| 2026-11-23 | 월 | 30 | Thinking in Systems p. 154-172 (19 pp) | |
| 2026-11-24 | 화 | 30 | Thinking in Systems p. 173-192 (20 pp) | |
| 2026-11-25 | 수 | 20 | Thinking in Systems p. 193-205 (13 pp) | |
| 2026-11-26 | 목 | 15 | Thinking in Systems p. 206-215 (10 pp) | 늦은 교대 |
| 2026-11-27 | 금 | 25 | Thinking in Systems p. 216-218 (3 pp); Middlemarch p. 1-14 (14 pp) | |
| 2026-11-28 | 토 | 60 | Middlemarch p. 15-54 (40 pp) | |
| 2026-11-29 | 일 | - | - | 보충 / 여유 (45분 가능); Thinking in Systems 완료 날짜 |
| 2026-11-30 | 월 | 30 | Middlemarch p. 55-74 (20 pp) | |
| 2026-12-01 | 화 | 30 | Middlemarch p. 75-94 (20 pp) | 마감일 Thinking in Systems - 팀 독서 그룹 |
| 2026-12-02 | 수 | 20 | Middlemarch p. 95-107 (13 pp) | |
| 2026-12-03 | 목 | 30 | Middlemarch p. 108-128 (21 pp) | |
| 2026-12-04 | 금 | 25 | Middlemarch p. 129-144 (16 pp) | |
| 2026-12-05 | 토 | 120 | Middlemarch p. 145-224 (80 pp) | 조용한 토요일; 2 세션 |
| 2026-12-06 | 일 | - | - | 보충 / 여유 (45분 가능) |
| 2026-12-07 | 월 | 30 | Middlemarch p. 225-244 (20 pp) | |
| 2026-12-08 | 화 | 30 | Middlemarch p. 245-265 (21 pp) | |
| 2026-12-09 | 수 | 20 | Middlemarch p. 266-278 (13 pp) | |
| 2026-12-10 | 목 | 30 | Middlemarch p. 279-298 (20 pp) | |
| 2026-12-11 | 금 | 25 | Middlemarch p. 299-315 (17 pp) | |
| 2026-12-12 | 토 | 60 | Middlemarch p. 316-355 (40 pp) | |
| 2026-12-13 | 일 | - | - | 보충 / 여유 (45분 가능) |
| 2026-12-14 | 월 | 30 | Middlemarch p. 356-375 (20 pp) | |
| 2026-12-15 | 화 | 30 | Middlemarch p. 376-395 (20 pp) | Middlemarch 완료 날짜 |
| 2026-12-16 | 수 | 20 | Middlemarch p. 396-408 (13 pp) | |
| 2026-12-17 | 목 | 30 | Middlemarch p. 409-428 (20 pp) | 마감일 Middlemarch - 고전 클럽 |
| 2026-12-18 | 금 | 25 | Middlemarch p. 429-445 (17 pp) | |
| 2026-12-19 | 토 | 60 | Middlemarch p. 446-485 (40 pp) | |
| 2026-12-20 | 일 | - | - | 보충 / 여유 (45분 가능) |
| 2026-12-21 | 월 | 30 | Middlemarch p. 486-505 (20 pp) | |
| 2026-12-22 | 화 | 30 | Middlemarch p. 506-525 (20 pp) | |
| 2026-12-23 | 수 | 20 | Middlemarch p. 526-539 (14 pp) | |
| 2026-12-24 | 목 | 30 | Middlemarch p. 540-559 (20 pp) | |
| 2026-12-25 | 금 | 25 | Middlemarch p. 560-575 (16 pp) | |
| 2026-12-26 | 토 | 60 | Middlemarch p. 576-615 (40 pp) | |
| 2026-12-27 | 일 | - | - | 보충 / 여유 (45분 가능) |
| 2026-12-28 | 월 | 30 | Middlemarch p. 616-635 (20 pp) | |
| 2026-12-29 | 화 | 30 | Middlemarch p. 636-656 (21 pp) | |
| 2026-12-30 | 수 | 20 | Middlemarch p. 657-669 (13 pp) | |
| 2026-12-31 | 목 | 30 | Middlemarch p. 670-689 (20 pp) | |
| 2027-01-01 | 금 | 25 | Middlemarch p. 690-706 (17 pp) | |
| 2027-01-02 | 토 | 60 | Middlemarch p. 707-746 (40 pp) | |
| 2027-01-03 | 일 | - | - | 보충 / 여유 (45분 가능) |
| 2027-01-04 | 월 | 30 | Middlemarch p. 747-766 (20 pp) | |
| 2027-01-05 | 화 | 30 | Middlemarch p. 767-786 (20 pp) | |
| 2027-01-06 | 수 | 20 | Middlemarch p. 787-799 (13 pp) | |
| 2027-01-07 | 목 | 30 | Middlemarch p. 800-819 (20 pp) | |
| 2027-01-08 | 금 | 25 | Middlemarch p. 820-836 (17 pp) | |
| 2027-01-09 | 토 | 60 | Middlemarch p. 837-876 (40 pp) | |
| 2027-01-10 | 일 | - | - | 보충 / 여유 (45분 가능) |
| 2027-01-11 | 월 | 30 | Middlemarch p. 877-880 (4 pp); Piranesi (오디오북) 0:00-0:30 | |
| 2027-01-12 | 화 | 30 | Piranesi (오디오북) 0:30-1:08 | |
| 2027-01-13 | 수 | 20 | Piranesi (오디오북) 1:08-1:33 | |
| 2027-01-14 | 목 | 30 | Piranesi (오디오북) 1:33-2:10 | |
| 2027-01-15 | 금 | 25 | Piranesi (오디오북) 2:10-2:41 | |
| 2027-01-16 | 토 | 60 | Piranesi (오디오북) 2:41-3:56 | |
| 2027-01-17 | 일 | - | - | 보충 / 여유 (45분 가능) |
| 2027-01-18 | 월 | 30 | Piranesi (오디오북) 3:56-4:34 | |
| 2027-01-19 | 화 | 30 | Piranesi (오디오북) 4:34-5:11 | |
| 2027-01-20 | 수 | 20 | Piranesi (오디오북) 5:11-5:36 | |
| 2027-01-21 | 목 | 30 | Piranesi (오디오북) 5:36-6:14 | |
| 2027-01-22 | 금 | 21 | Piranesi (오디오북) 6:14-6:40 | |
## 주간 체크포인트 (일요일 밤까지 있어야 할 위치)
- 2026-11-08: The Remains of the Day p. 148
- 2026-11-15: Thinking in Systems p. 24
- 2026-11-22: Thinking in Systems p. 153
- 2026-11-29: Middlemarch p. 54
- 2026-12-06: Middlemarch p. 224
- 2026-12-13: Middlemarch p. 355
- 2026-12-20: Middlemarch p. 485
- 2026-12-27: Middlemarch p. 615
- 2027-01-03: Middlemarch p. 746
- 2027-01-10: Middlemarch p. 876
- 2027-01-17: Piranesi (오디오북) 3:56
## 뒤처졌을 때
- The Remains of the Day: 하루 놓치면 약 27페이지 손해. 여유일(들) 2026-11-08에 보충하거나, 다음 3일의 독서일 각각에 10페이지 추가. 언제든 `--from <날짜> --at "The Remains of the Day=<페이지>"`로 재계획.
- Thinking in Systems: 하루 놓치면 약 16페이지 손해. 여유일(들) 2026-11-22, 2026-11-29에 보충하거나, 다음 3일의 독서일 각각에 6페이지 추가. 언제든 `--from <날짜> --at "Thinking in Systems=<페이지>"`로 재계획.
- Middlemarch: 하루 놓치면 약 23페이지 손해. 여유일(들) 2026-11-29, 2026-12-06, 2026-12-13에 보충하거나, 다음 3일의 독서일 각각에 8페이지 추가. 언제든 `--from <날짜> --at "Middlemarch=<페이지>"`로 재계획.
FILE:scripts/reading_plan.py
#!/usr/bin/env python3
"""독서 계획 및 페이지 속도 플래너 (표준 라이브러리만, Python 3.8+).
책 목록(페이지, 단어 수 또는 오디오북 길이), 독서 속도 및 매일 가용한 분을 페이지 범위가 있는 일별 계획으로 변환하여 모든 마감일(북클럽, 강의, 시험)을 충족하고, 각 마감일 전에 버퍼 일수와 주중 보충일을 유지하며, 부하를 고르게 분산하고 뒤처졌을 때 해야 할 일을 알려줍니다.
사용법:
reading_plan.py PLAN.json [--mode even|asap] [--from YYYY-MM-DD] [--at "Title=page"]... [--json]
reading_plan.py --self-test
종료: 0 = 모든 마감일 충족, 1 = 최소 한 권의 책을 마감일까지 끝낼 수 없음, 2 = 잘못된 입력.
"""
import argparse, datetime, json, math, re, sys
DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
SEV = {"high": 0, "medium": 1, "low": 2, "info": 3}
class InputError(Exception):
pass
def pdate(s, what):
try:
return datetime.date.fromisoformat(str(s))
except ValueError:
raise InputError("%s는 YYYY-MM-DD 형식이어야 합니다, 받은 값: %r" % (what, s))
def expand_dates(spec):
spec = str(spec)
if ".." in spec:
a, b = spec.split("..", 1)
a, b = pdate(a.strip(), "날짜 범위"), pdate(b.strip(), "날짜 범위")
if b < a:
raise InputError("날짜 범위 %s가 시작 전에 끝납니다" % spec)
return [a + datetime.timedelta(days=i) for i in range((b - a).days + 1)]
return [pdate(spec.strip(), "날짜")]
# ---------------------------------------------------------------- 모델
def load(cfg, start_override=None, at=None):
if not isinstance(cfg, dict) or not isinstance(cfg.get("books"), list) or not cfg["books"]:
raise InputError("계획에는 비어 있지 않은 'books' 목록이 필요합니다")
reader = cfg.get("reader") or {}
wpm = float(reader.get("words_per_minute", 230))
wpp_default = float(reader.get("words_per_page", 275))
pph = reader.get("pages_per_hour")
if wpm <= 0 or wpp_default <= 0:
raise InputError("words_per_minute와 words_per_page는 양수여야 합니다")
start = pdate(start_override or cfg.get("start") or datetime.date.today().isoformat(), "start")
mins = cfg.get("minutes")
if isinstance(mins, (int, float)):
mins = {d: mins for d in DAYS}
if not isinstance(mins, dict):
raise InputError("'minutes'는 숫자 또는 {mon: .., tue: ..} 형식이어야 합니다")
mins = {k.lower()[:3]: float(v) for k, v in mins.items()}
bad = set(mins) - set(DAYS)
if bad:
raise InputError("minutes에 알 수 없는 요일 키가 있습니다: %s" % ", ".join(sorted(bad)))
catch = {d.lower()[:3] for d in cfg.get("catch_up_days", [])}
exc = {}
for e in cfg.get("exceptions", []):
for d in expand_dates(e.get("dates") or e.get("date")):
exc[d] = (float(e.get("minutes", 0)), e.get("note", ""))
books = []
at = at or {}
for i, b in enumerate(cfg["books"], 1):
t = b.get("title") or "책 %d" % i
if "audio_minutes" in b:
speed = float(b.get("speed", 1.0))
total = float(b["audio_minutes"])
unit_min = 1.0 / speed # 오디오 1분당 내 시간(분)
units, kind = total, "audio"
done = float(at.get(t, b.get("start_minute", 0)))
else:
wpp = float(b.get("words_per_page") or (float(b["words"]) / float(b["pages"]) if b.get("words") and b.get("pages") else wpp_default))
pages = float(b["pages"]) if b.get("pages") else (float(b["words"]) / wpp if b.get("words") else None)
if not pages or pages <= 0:
raise InputError("책 %r에는 pages, words 또는 audio_minutes가 필요합니다" % t)
diff = float(b.get("difficulty", 1.0))
if pph and not b.get("words"):
unit_min = 60.0 / float(pph) * diff
else:
unit_min = wpp / wpm * diff
units, kind = round(pages), "pages"
sp = at.get(t, b.get("start_page", 1))
done = max(0.0, float(sp) - 1)
dl = pdate(b["deadline"], "%r의 마감일" % t) if b.get("deadline") else None
chapters = sorted(int(c) for c in b.get("chapters", []))
books.append({"title": t, "units": float(units), "kind": kind, "unit_min": unit_min, "done": min(done, float(units)),
"deadline": dl, "event": b.get("event", ""), "chapters": chapters, "order": i})
buf = int(cfg.get("buffer_days", 1))
return {"start": start, "mins": mins, "catch": catch, "exc": exc, "books": books, "buffer": buf,
"max_session": float(cfg.get("max_session_minutes", 0) or 0), "snap": int(cfg.get("chapter_snap_pages", 8)),
"name": cfg.get("name", "독서 계획"), "wpm": wpm}
def capacity(m, d):
"""(계획된 분, 보충일 포함 가용 분, 메모)."""
if d in m["exc"]:
v, note = m["exc"][d]
return v, v, note or ("독서 없음" if v == 0 else "")
wd = DAYS[d.weekday()]
v = m["mins"].get(wd, 0.0)
if wd in m["catch"]:
return 0.0, v, "보충일"
return v, v, ""
def plan(m, mode="even"):
F = []
books = [b for b in m["books"]]
for b in books:
b["need"] = (b["units"] - b["done"]) * b["unit_min"]
if b["need"] <= 0:
F.append(("info", "RP05", "%s는 이미 완료됨 - 제외" % b["title"]))
b["target"] = None
if b["deadline"] and b["deadline"] < m["start"]:
if b["need"] > 0:
F.append(("medium", "RP05", "%s: 마감일 %s이(가) 이미 지남 - 마감일 없이 계획" % (b["title"], b["deadline"])))
b["deadline"] = None
if b["deadline"]:
b["target"] = b["deadline"] - datetime.timedelta(days=m["buffer"])
if b["target"] < m["start"]:
b["target"] = m["start"]
F.append(("medium", "RP05", "%s: 마감일 %s이(가) %d일 버퍼에 너무 가까움 - 마감일까지 계획" % (b["title"], b["deadline"], m["buffer"])))
todo = [b for b in books if b["need"] > 0]
dated = sorted([b for b in todo if b["target"]], key=lambda b: (b["target"], b["order"]))
undated = [b for b in todo if not b["target"]]
seq = dated + undated
last = max([b["deadline"] for b in dated] or [m["start"]])
# 지평선: 전체 용량으로 모든 것을 처리할 충분한 일수 + 여유
weekly = sum(m["mins"].get(d, 0) for d in DAYS if d not in m["catch"]) or 1
total_need = sum(b["need"] for b in seq)
horizon_end = max(last, m["start"]) + datetime.timedelta(days=int(total_need / weekly * 7) + 60)
days, d = [], m["start"]
while d <= horizon_end:
p, a, note = capacity(m, d)
days.append({"date": d, "cap": p, "avail": a, "note": note, "planned": 0.0, "items": []})
d += datetime.timedelta(days=1)
idx = {x["date"]: i for i, x in enumerate(days)}
# 세그먼트별 부하 계수 (균등 모드): 모든 목표를 충족하는 용량의 가장 작은 균등 몫
load = [1.0] * len(days)
if mode == "even" and dated:
seg_start, k0 = 0, 0
while k0 < len(dated):
best, bk = 0.0, k0
cum = 0.0
for k in range(k0, len(dated)):
cum += dated[k]["need"]
ti = idx[dated[k]["target"]]
cap = sum(x["cap"] for x in days[seg_start:ti + 1])
r = cum / cap if cap > 0 else float("inf")
if r >= best:
best, bk = r, k
L = min(best * 1.03, 1.0) # 챕터 끝 중단 및 반올림을 위한 3% 여유
end = idx[dated[bk]["target"]]
for i in range(seg_start, end + 1):
load[i] = L
seg_start, k0 = end + 1, bk + 1
# 마지막 마감일 이후: 지금까지 계획의 평균 부하로 마감일 없는 책 (최소 0.5)
tail = max(0.5, max(load[:seg_start] or [1.0]))
for i in range(seg_start, len(days)):
load[i] = tail if undated else 1.0
# 할당 (가장 이른 마감일 우선), 각 책의 위치 추적
qi = 0
pos = {b["title"]: b["done"] for b in seq}
for i, day in enumerate(days):
budget = day["cap"] * load[i]
while budget > 1e-9 and qi < len(seq):
b = seq[qi]
remaining = (b["units"] - pos[b["title"]]) * b["unit_min"]
use = min(budget, remaining)
before = pos[b["title"]]
after = before + use / b["unit_min"]
if after > b["units"] - 1e-6:
after = b["units"]
elif b["kind"] == "pages":
snapped = snap(after, b, m["snap"], use / b["unit_min"])
if snapped != after:
after = snapped
use = (after - before) * b["unit_min"]
budget = use # 챕터 끝에서 중단: 하루가 여기서 끝남
pos[b["title"]] = after
day["items"].append({"title": b["title"], "from": before, "to": after, "kind": b["kind"], "minutes": use})
day["planned"] += use
budget -= use
if after >= b["units"] - 1e-6:
b["finish"] = day["date"]; qi += 1
elif budget <= 1e-9 or use <= 1e-9:
break
if qi >= len(seq):
break
days = [x for x in days if x["date"] <= max([b.get("finish", m["start"]) for b in seq] + [last])]
for b in seq:
b.setdefault("finish", None)
# 발견 사항
for b in dated:
f = b["finish"]
if f is None or f > b["deadline"]:
avail = [x for x in days if x["date"] <= b["deadline"]]
share = sum(it["minutes"] for x in avail for it in x["items"] if it["title"] == b["title"])
short_min = b["need"] - share
# 마감일까지 사용 가능한 보충/버퍼 시간도 계산
reading_days = sum(1 for x in avail if x["avail"] > 0) or 1
short_units = short_min / b["unit_min"]
msg = "%s은(는) %s%s까지 끝낼 수 없음: 현재 속도로 약 %d %s 부족; 예상 완료 %s. 옵션: 그때까지 %d일의 독서일 각각에 +%d분, 더 빨리 읽기, 약 %d %s 훑어보기 또는 건너뛰기, 또는 더 일찍 시작" % (
b["title"], b["deadline"], " (%s)" % b["event"] if b["event"] else "", math.ceil(short_units), "페이지" if b["kind"] == "pages" else "오디오 분",
f or "계획 범위를 벗어남", math.ceil(short_min / reading_days), reading_days, math.ceil(short_units), "페이지" if b["kind"] == "pages" else "오디오 분")
F.append(("high", "RP01", msg))
elif f > b["target"]:
F.append(("medium", "RP02", "%s은(는) %s에 완료, %s 전 %d일 버퍼 안 - 놓친 하루를 위한 여유 없음" % (b["title"], f, b["deadline"], m["buffer"])))
if mode == "even":
peak = max(load) if load else 0
if 0.9 <= peak <= 1.0 and not any(f[1] == "RP01" for f in F):
F.append(("medium", "RP04", "계획은 가장 바쁜 구간에서 계획된 독서 시간의 %d%%를 사용합니다 - 한 번의 나쁜 주가 버퍼로 밀어넣을 것입니다" % round(peak * 100)))
if m["max_session"]:
long = [x for x in days if x["planned"] > m["max_session"] + 0.5]
if long:
F.append(("low", "RP03", "%d일이 %d분 이상 계획됨 (예: %s: %d분) - 두 세션으로 분할" % (
len(long), m["max_session"], long[0]["date"], round(long[0]["planned"]))))
for b in seq:
if b["kind"] == "audio":
F.append(("info", "RP06", "%s은(는) 오디오북: 계획은 %s배속 청취 시간을 계산합니다; 통근과 집안일로 충당 가능" % (b["title"], ("%.2f" % (1 / b["unit_min"])).rstrip("0").rstrip("."))))
F.sort(key=lambda f: (SEV[f[0]], f[1]))
return {"days": days, "books": seq, "all_books": books, "findings": F, "load": load}
def snap(after, b, snap_pages, day_units):
"""하루의 중단점을 가까운 챕터 끝으로 이동 (snap_pages 및 그날의 25% 이내)."""
if not b["chapters"] or snap_pages <= 0:
return after
lim = min(snap_pages, max(1.0, day_units * 0.25))
ends = [c - 1 for c in b["chapters"] if c - 1 > 0]
best = None
for e in ends:
if abs(e - after) <= lim and e > after - day_units + 0.5 and (best is None or abs(e - after) < abs(best - after)):
best = e
return float(best) if best is not None else after
# ---------------------------------------------------------------- 렌더링
def fmt_item(it):
if it["kind"] == "audio":
return "%s %s-%s" % (it["title"], hm(it["from"]), hm(it["to"]))
a = int(round(it["from"])) + 1
z = int(round(it["to"]))
if z < a:
z = a
return "%s p. %d-%d (%d pp)" % (it["title"], a, z, z - a + 1)
def hm(x):
x = int(round(x))
return "%d:%02d" % (x // 60, x % 60)
def checkpoints(r):
out = []
pos = {}
for x in r["days"]:
for it in x["items"]:
pos[it["title"]] = it
if x["date"].weekday() == 6 and pos: # 일요일
out.append((x["date"], "; ".join("%s %s" % (t, ("p. %d" % round(it["to"]) if it["kind"] == "pages" else hm(it["to"]))) for t, it in pos.items() if it["to"] < next(b["units"] for b in r["books"] if b["title"] == t) - 1e-6) or "현재 모든 책 완료"))
return out
def report(r, m, mode):
hi = [f for f in r["findings"] if f[0] == "high"]
out = ["# %s" % m["name"], ""]
tot_min = sum(x["planned"] for x in r["days"])
out.append("시작 %s, %s 모드, 각 마감일 전 %d일 버퍼, 보충일: %s. 계획된 독서: %d일 동안 %d시간 %02d분." % (
m["start"], mode, m["buffer"], ", ".join(sorted(m["catch"], key=DAYS.index)) or "없음", tot_min // 60, tot_min % 60, len(r["days"])))
out += ["", "**판정: %s**" % ("%d권의 책을 마감일까지 끝낼 수 없음 - 계획 변경" % len(hi) if hi else "모든 마감일 충족"), ""]
out += ["## 책", "", "| 책 | 남음 | 예상 시간 | 마감일 | 완료 날짜 | 계획 완료 | 여유 일수 | 독서일당 평균 |", "|---|---|---|---|---|---|---|---|"]
for b in r["all_books"]:
left = b["units"] - b["done"]
rd = [x for x in r["days"] if any(it["title"] == b["title"] for it in x["items"])]
avg = left / len(rd) if rd else 0
spare = (b["deadline"] - b["finish"]).days if b.get("finish") and b["deadline"] else None
out.append("| %s | %s | %s | %s | %s | %s | %s | %s |" % (
b["title"], ("%d pp" % round(left)) if b["kind"] == "pages" else hm(left) + " 오디오", hm(b["need"]) + " h",
"%s %s" % (b["deadline"], "(%s)" % b["event"] if b["event"] else "") if b["deadline"] else "-",
b["target"] or "-", b.get("finish") or ("완료" if b["need"] <= 0 else "제시간에 못 끝남"),
"-" if spare is None else spare, ("%.0f pp" % avg) if b["kind"] == "pages" else hm(avg)))
out += ["", "## 발견 사항", ""]
out += ["- **%s %s** %s" % (s.upper(), c, msg) for s, c, msg in r["findings"]] or ["- 없음"]
out += ["", "## 일별", "", "| 날짜 | 요일 | 분 | 읽을 내용 | 메모 |", "|---|---|---|---|---|"]
events = {}
for b in r["all_books"]:
if b["deadline"]:
events.setdefault(b["deadline"], []).append("마감일 %s%s" % (b["title"], " - " + b["event"] if b["event"] else ""))
if b["target"] and b["target"] != b["deadline"]:
events.setdefault(b["target"], []).append("%s 완료 날짜" % b["title"])
for x in r["days"]:
note = [x["note"]] if x["note"] else []
if x["note"] == "보충일" or (x["planned"] < 0.5 and x["avail"] > 0):
note = ["보충 / 여유 (%d분 가능)" % x["avail"]] if x["avail"] > 0 else note
note += events.get(x["date"], [])
if m["max_session"] and x["planned"] > m["max_session"] + 0.5:
note.append("2 세션")
out.append("| %s | %s | %s | %s | %s |" % (x["date"], x["date"].strftime("%a"), round(x["planned"]) if x["planned"] >= 0.5 else "-",
"; ".join(fmt_item(it) for it in x["items"]) or "-", "; ".join(note)))
cps = checkpoints(r)
if cps:
out += ["", "## 주간 체크포인트 (일요일 밤까지 있어야 할 위치)", ""]
out += ["- %s: %s" % (d, s) for d, s in cps]
out += ["", "## 뒤처졌을 때", ""]
for b in r["books"]:
if b["kind"] != "pages" or not b["deadline"]:
continue
rd = [x for x in r["days"] if any(it["title"] == b["title"] for it in x["items"])]
if not rd:
continue
day_pp = sum(it["to"] - it["from"] for x in rd for it in x["items"] if it["title"] == b["title"]) / len(rd)
spare = [x for x in r["days"] if x["date"] <= b["deadline"] and x["date"] >= rd[0]["date"] and x["avail"] > 0 and x["planned"] < 0.5]
out.append("- %s: 하루 놓치면 약 %d페이지 손해. %s에 보충하거나, 다음 3일의 독서일 각각에 %d페이지 추가. 언제든 `--from <날짜> --at \"%s=<페이지>\"`로 재계획." % (
b["title"], round(day_pp), ("여유일(들) " + ", ".join(str(x["date"]) for x in spare[:3])) if spare else "마감일 전 여유일 없음", math.ceil(day_pp / 3), b["title"]))
out.append("")
return "\n".join(out)
# ---------------------------------------------------------------- 자체 테스트
def self_test():
tests = []
def t(name, cond):
tests.append((name, bool(cond)))
def run(cfg, mode="even", **kw):
m = load(cfg, **kw)
return plan(m, mode), m
base = {"start": "2026-11-02", "reader": {"words_per_minute": 250, "words_per_page": 250}, "minutes": 30, "buffer_days": 0}
r, m = run(dict(base, books=[{"title": "A", "pages": 60, "deadline": "2026-11-08"}]), "asap")
t("asap: 분당 1페이지에서 하루 30페이지는 2일째 완료", r["books"][0]["finish"] == datetime.date(2026, 11, 3))
r, m = run(dict(base, books=[{"title": "A", "pages": 70, "deadline": "2026-11-08"}]))
t("even: 주에 걸쳐 분산 (~하루 10분 + 3% 여유)", r["books"][0]["finish"] >= datetime.date(2026, 11, 7) and 10 <= r["days"][0]["planned"] <= 10.31)
r, m = run(dict(base, books=[{"title": "A", "pages": 300, "deadline": "2026-11-08"}]))
t("불가능 -> RP01 높음", any(f[1] == "RP01" and f[0] == "high" for f in r["findings"]))
t("RP01이 추가 분 제안", any("+13분" in f[2] for f in r["findings"] if f[1] == "RP01"))
r, m = run(dict(base, buffer_days=2, books=[{"title": "A", "pages": 70, "deadline": "2026-11-08"}]))
t("버퍼 일수: 2일 일찍 완료", r["books"][0]["finish"] == datetime.date(2026, 11, 6))
r, m = run(dict(base, catch_up_days=["sun"], books=[{"title": "A", "pages": 60, "deadline": "2026-11-08"}]))
t("보충 일요일에는 계획된 독서 없음", r["days"][-1]["planned"] == 0 and r["books"][0]["finish"] == datetime.date(2026, 11, 7))
r, m = run(dict(base, exceptions=[{"dates": "2026-11-03..2026-11-04", "minutes": 0, "note": "여행"}], books=[{"title": "A", "pages": 50, "deadline": "2026-11-08"}]))
t("예외 범위에는 독서 없음", all(x["planned"] == 0 for x in r["days"][1:3]))
two = [{"title": "늦음", "pages": 30, "deadline": "2026-11-20"}, {"title": "곧", "pages": 60, "deadline": "2026-11-04"}]
r, m = run(dict(base, books=two))
t("가장 이른 마감일 우선", r["days"][0]["items"][0]["title"] == "곧")
t("빠듯한 구간 후 더 가벼운 부하", r["load"][0] > 0.6 and r["load"][5] < 0.2)
r, m = run(dict(base, books=[{"title": "A", "pages": 70, "deadline": "2026-11-08"}]), at={"A": 36})
t("--at이 페이지에서 재개", abs(sum(x["planned"] for x in r["days"]) - 35) < 0.01)
r, m = run(dict(base, books=[{"title": "W", "words": 25000, "deadline": "2026-11-08"}]))
t("단어 수 -> words_per_page로 페이지 변환", r["all_books"][0]["units"] == 100)
r, m = run(dict(base, books=[{"title": "H", "pages": 70, "difficulty": 1.5, "deadline": "2026-11-08"}]))
t("난이도가 시간을 조정", abs(r["books"][0]["need"] - 105) < 0.01)
r, m = run(dict(base, books=[{"title": "Au", "audio_minutes": 300, "speed": 1.5, "deadline": "2026-11-08"}]))
t("1.5배속 오디오북 = 200분", abs(r["books"][0]["need"] - 200) < 0.01 and any(f[1] == "RP06" for f in r["findings"]))
r, m = run(dict(base, books=[{"title": "C", "pages": 70, "deadline": "2026-11-08", "chapters": [1, 12, 23, 31, 42, 55]}]))
ends = [round(it["to"]) for x in r["days"] for it in x["items"]]
t("챕터 스냅이 챕터 끝에서 중단", 11 in ends and 22 in ends and ends[-1] == 70)
t("책당 하루 하나의 항목", all(len({it["title"] for it in x["items"]}) == len(x["items"]) for x in r["days"]))
t("페이지 범위가 겹치지 않음", all(fmt_item(it) for x in r["days"] for it in x["items"]) and
[int(round(it["from"])) for x in r["days"] for it in x["items"]][1:] == [int(round(it["to"])) for x in r["days"] for it in x["items"]][:-1])
r, m = run(dict(base, max_session_minutes=20, minutes=60, books=[{"title": "A", "pages": 300, "deadline": "2026-11-08"}]))
t("긴 세션 -> RP03", any(f[1] == "RP03" for f in r["findings"]))
r, m = run(dict(base, books=[{"title": "A", "pages": 200, "deadline": "2026-11-08"}]))
t("빠듯한 계획 -> RP04 또는 RP01", any(f[1] in ("RP04", "RP01") for f in r["findings"]))
r, m = run(dict(base, books=[{"title": "A", "pages": 30}, {"title": "B", "pages": 60, "deadline": "2026-11-05"}]))
t("마감일 없는 책은 마감일 있는 책 뒤에", r["books"][-1]["title"] == "A" and r["books"][-1]["finish"])
r, m = run(dict(base, books=[{"title": "A", "pages": 20, "start_page": 21}]))
t("완료된 책 -> RP05", any(f[1] == "RP05" for f in r["findings"]))
t("pages_per_hour", abs(load(dict(base, reader={"pages_per_hour": 30}, books=[{"title": "P", "pages": 30}]))["books"][0]["unit_min"] - 2) < 1e-9)
for bad in [dict(base, books=[]), dict(base, books=[{"title": "x"}]), dict(base, minutes={"funday": 3}, books=[{"title": "a", "pages": 3}]),
dict(base, books=[{"title": "a", "pages": 3, "deadline": "2026-13-01"}])]:
try:
run(bad); okk = False
except InputError:
okk = True
t("잘못된 입력 거부: %s" % (list(bad.get("minutes", {})) if isinstance(bad.get("minutes"), dict) else (bad["books"][0] if bad["books"] else "책 없음")), okk)
r, m = run(dict(base, books=[{"title": "오래됨", "pages": 30, "deadline": "2026-10-01"}]))
t("지난 마감일 -> RP05, 마감일 없이 계획", any(f[1] == "RP05" for f in r["findings"]) and r["books"][0]["finish"])
rep = report(*run(dict(base, books=two)), "even")
t("보고서에 판정, 일일 표 및 보충 조언 포함", "판정" in rep and "| 날짜 |" in rep and "뒤처졌을 때" in rep)
for name, okk in tests:
print(("PASS " if okk else "FAIL ") + name)
n = sum(1 for _, okk in tests if okk)
print("%d/%d 통과" % (n, len(tests)))
return 0 if n == len(tests) else 1
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("plan", nargs="?")
ap.add_argument("--mode", choices=["even", "asap"], default="even")
ap.add_argument("--from", dest="start", help="이 날짜부터 다시 계획 (YYYY-MM-DD)")
ap.add_argument("--at", action="append", default=[], help='"Title=page" (또는 오디오북의 경우 "Title=minute"): 현재 위치')
ap.add_argument("--json", action="store_true")
ap.add_argument("--self-test", action="store_true")
a = ap.parse_args(argv)
if a.self_test:
return self_test()
if not a.plan:
ap.error("계획 JSON 필요 (또는 --self-test)")
try:
try:
cfg = json.load(open(a.plan, encoding="utf-8"))
except (OSError, ValueError) as e:
raise InputError(str(e))
at = {}
for s in a.at:
if "=" not in s:
raise InputError("--at에는 Title=page 형식이 필요합니다, 받은 값: %r" % s)
k, v = s.rsplit("=", 1)
try:
at[k.strip()] = float(v)
except ValueError:
raise InputError("--at 페이지는 숫자여야 합니다, 받은 값: %r" % v)
titles = {b.get("title") for b in cfg.get("books", [])} if isinstance(cfg, dict) else set()
for k in at:
if k not in titles:
raise InputError("--at: %r 제목의 책이 없습니다" % k)
m = load(cfg, a.start, at)
r = plan(m, a.mode)
except (InputError, KeyError, TypeError, ValueError) as e:
print("오류: %s" % e, file=sys.stderr)
return 2
if a.json:
out = {"books": [{"title": b["title"], "deadline": str(b["deadline"]) if b["deadline"] else None, "finish": str(b.get("finish")) if b.get("finish") else None,
"minutes_needed": round(b["need"])} for b in r["all_books"]],
"days": [{"date": str(x["date"]), "minutes": round(x["planned"]), "read": [fmt_item(it) for it in x["items"]]} for x in r["days"]],
"findings": [{"severity": s, "code": c, "message": msg} for s, c, msg in r["findings"]]}
print(json.dumps(out, indent=2))
else:
print(report(r, m, a.mode))
return 1 if any(f[0] == "high" for f in r["findings"]) else 0
if __name__ == "__main__":
sys.exit(main())
전체 프롬프트를 보려면 로그인하세요
Continue with:
By logging in, you agree to our Terms of Use and Privacy Policy
사용법
이 프롬프트는 productivity와 함께 사용하도록 설계되었습니다. 위의 프롬프트 내용을 복사하여 원하는 AI 도구에 붙여넣으세요.
최상의 결과를 얻으려면 자리 표시자(대괄호 또는 대문자로 표시)를 특정 요구 사항으로 사용자 지정할 수 있습니다.
토론
댓글 0개