Skip to content

Commit 95e1098

Browse files
committed
feat: add new blog posts and update content configuration
1 parent 72cd783 commit 95e1098

9 files changed

Lines changed: 1191 additions & 4 deletions

File tree

‎admin/src/main.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ function renderEditor(post?: Post) {
284284
currentView = 'editor';
285285
activePost = post;
286286
const value: Post = post ?? { id: '', slug: '', title: '', description: '', pubDate: today, category: 'Study', tags: [], body: '', version: 0, desiredVisibility: 'draft', status: 'draft', updatedAt: new Date().toISOString() };
287-
shell(`<div class="page-heading"><div><p class="eyebrow">WRITING DESK</p><h1>${post ? '글 수정' : '새 글'}</h1></div><span id="save-status" class="muted">저장되지 않음</span></div><form id="editor-form" class="editor"><label>제목<input name="title" required value="${escape(value.title)}"></label><label>설명<input name="description" value="${escape(value.description)}"></label><div class="form-row"><label>발행일<input name="pubDate" type="date" value="${value.pubDate.slice(0, 10)}"></label><label>카테고리<select name="category">${['Git','일상','project','Study','MacOS','Algorithm','uncategorized'].map(category => `<option ${category === value.category ? 'selected' : ''}>${category}</option>`).join('')}</select></label></div><label>태그<input name="tags" value="${escape(value.tags.join(', '))}" placeholder="쉼표로 구분"></label><label>본문 <textarea name="body" required>${escape(value.body)}</textarea></label><div class="editor-actions"><button type="button" id="back">목록</button><button type="submit" class="primary">저장</button><button type="button" id="publish" class="accent">${value.status === 'published' ? '초안으로 전환' : '발행'}</button></div></form><div class="preview"><p class="eyebrow">PREVIEW</p><div id="preview-body" class="preview-body"></div></div>`);
287+
shell(`<div class="page-heading"><div><p class="eyebrow">WRITING DESK</p><h1>${post ? '글 수정' : '새 글'}</h1></div><span id="save-status" class="muted">저장되지 않음</span></div><form id="editor-form" class="editor"><label>제목<input name="title" required value="${escape(value.title)}"></label><label>설명<input name="description" value="${escape(value.description)}"></label><div class="form-row"><label>발행일<input name="pubDate" type="date" value="${value.pubDate.slice(0, 10)}"></label><label>카테고리<select name="category">${['Git','일상','project','Study','MacOS','Algorithm','Library','uncategorized'].map(category => `<option ${category === value.category ? 'selected' : ''}>${category}</option>`).join('')}</select></label></div><label>태그<input name="tags" value="${escape(value.tags.join(', '))}" placeholder="쉼표로 구분"></label><label>본문 <textarea name="body" required>${escape(value.body)}</textarea></label><div class="editor-actions"><button type="button" id="back">목록</button><button type="submit" class="primary">저장</button><button type="button" id="publish" class="accent">${value.status === 'published' ? '초안으로 전환' : '발행'}</button></div></form><div class="preview"><p class="eyebrow">PREVIEW</p><div id="preview-body" class="preview-body"></div></div>`);
288288
const form = document.querySelector<HTMLFormElement>('#editor-form')!;
289289
form.addEventListener('input', () => { editorDirty = true; });
290290
const body = form.elements.namedItem('body') as HTMLTextAreaElement;

‎admin/worker/posts.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export async function discardPost(request: Request, env: Env, id: string) {
3131
} catch { return error('SOURCE_READ_FAILED', '원본을 확인하지 못해 작업본을 보존했습니다.', 502); }
3232
}
3333

34-
const categories = new Set(['Git', '일상', 'project', 'Study', 'MacOS', 'Algorithm', 'uncategorized']);
34+
const categories = new Set(['Git', '일상', 'project', 'Study', 'MacOS', 'Algorithm', 'Library', 'uncategorized']);
3535
const slugify = (title: string) => title.toLowerCase().normalize('NFKD').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 100) || `post-${Date.now()}`;
3636
const validInput = (input: any) => typeof input?.title === 'string' && input.title.trim() && typeof input?.description === 'string' && /^\d{4}-\d{2}-\d{2}/.test(input.pubDate) && categories.has(input.category) && Array.isArray(input.tags) && input.tags.every((tag: unknown) => typeof tag === 'string') && typeof input.body === 'string' && new TextEncoder().encode(input.body).byteLength <= 1048576;
3737
const row = (value: any) => ({ id: value.id, slug: value.slug, title: value.title, description: value.description, pubDate: value.pub_date, category: value.category, tags: JSON.parse(value.tags_json), body: value.body, version: value.version, desiredVisibility: value.desired_visibility, status: value.desired_visibility === 'published' && value.published_version !== value.version ? 'published_with_draft' : value.desired_visibility, updatedAt: value.updated_at, repoPath: value.repo_path });

‎src/domains/blog/content/config.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { z } from 'astro:content';
22

3-
const categories = ['Git', '일상', 'project', 'Study', 'MacOS', 'Algorithm', 'uncategorized'] as const;
3+
const categories = ['Git', '일상', 'project', 'Study', 'MacOS', 'Algorithm', 'Library', 'uncategorized'] as const;
44

55
export const postSchema = z.object({
66
title: z.string().min(1),
77
description: z.string(),
88
pubDate: z.coerce.date(),
99
category: z.enum(categories),
1010
tags: z.array(z.string()),
11+
slug: z.string().min(1).optional(),
1112
draft: z.boolean().default(false),
1213
});
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
interface ContentLayerEntry {
22
id: string;
3+
data?: { slug?: string };
34
}
45

56
export function getPostSlug(post: ContentLayerEntry): string {
6-
return post.id.split('/').pop()?.replace(/\.(?:md|mdx)$/i, '') ?? post.id;
7+
return post.data?.slug ?? post.id.split('/').pop()?.replace(/\.(?:md|mdx)$/i, '') ?? post.id;
78
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
---
2+
title: "내일의 내가 오늘 밤마다 전화를 건다 — 1화. 오른쪽으로 걷지 마"
3+
description: "밤 11시 17분, 내 번호로 전화가 왔다. 상대는 내일 아침 내가 어디서 다칠지를 알고 있었다."
4+
pubDate: 2026-09-10T13:40:00.000Z
5+
category: "Library"
6+
tags: ["소설", "미스터리", "내일의 내가 오늘 밤마다 전화를 건다"]
7+
slug: "tomorrow-calls-one"
8+
draft: false
9+
---
10+
11+
내 번호로 걸려 온 전화라서 받았다.
12+
13+
모르는 번호였으면 무시했을 것이다. 그날도 카드사에서 세 번, 인터넷을 바꿔 준다는 곳에서 두 번 전화가 왔다. 내 번호를 누군가 잘못 건드린 줄 알았다.
14+
15+
밤 11시 17분이었다. 나는 싱크대 앞에서 냉동만두 봉지 뒷면을 읽고 있었다. 전자레인지로 조리하면 맛없을 거라는 걸 알면서도, 프라이팬을 씻는 일이 더 싫었다.
16+
17+
“여보세요.”
18+
19+
상대는 한동안 숨만 쉬었다. 코가 막힌 사람처럼 입으로 짧게 들이마셨다.
20+
21+
“만두 내려놔.”
22+
23+
나는 베란다 쪽을 봤다. 맞은편 건물은 창문마다 커튼이 쳐져 있었다. 주방 블라인드를 내리지 않은 건 내 쪽이었다.
24+
25+
“누구세요.”
26+
27+
“전자레인지에 돌려. 어차피 프라이팬 안 씻을 거잖아.”
28+
29+
장난을 칠 만한 사람은 동생 수연밖에 없었다. 그런데 남자 목소리였다. 어디선가 들어 봤지만 처음에는 누구인지 떠오르지 않았다. 내 목소리는 내가 듣는 것보다 훨씬 납작했다. 예전에 면접 연습을 녹음했다가 바로 지웠던 기억이 났다.
30+
31+
나는 만두를 내려놓았다.
32+
33+
“너 누구야.”
34+
35+
“내일의 너.”
36+
37+
웃음이 나왔다. 상대는 웃지 않았다.
38+
39+
“초등학교 4학년 때 수연이가 베란다에 갇혔어. 네가 문 잠갔지. 엄마한테는 저절로 잠겼다고 했고.”
40+
41+
나는 식탁 의자를 당겨 앉았다.
42+
43+
“그걸 어떻게 알아.”
44+
45+
“수연이가 유리를 두 번 두드리고, 쉬었다가 한 번 더 두드렸어. 울면 네가 더 안 열어 줄까 봐 울지도 못했어.”
46+
47+
의자 다리 끄는 소리가 상대편에서도 났다. 내가 낸 소리보다 반 박자 늦었다. 통화가 울리는 모양이었다.
48+
49+
“걔한테 들었어?”
50+
51+
“수연이는 그날 기억 못 해. 너만 기억하지.”
52+
53+
나는 반박하려다가 입을 다물었다. 수연에게 물어본 적은 없었다. 기억하지 않기를 바랐던 건 맞았다.
54+
55+
“지금부터 잘 들어. 내일 출근할 때 편의점 앞에서 길 건너지 말고, 처음부터 왼쪽 인도로 가.”
56+
57+
“뭐?”
58+
59+
“8시 42분에 냉장 탑차가 후진해. 편의점 앞 파란 천막 기둥을 치고. 너는 소리 듣고 돌아보다가 오른손을 짚어.”
60+
61+
“…….”
62+
63+
“손목만 부러진 줄 알았는데 엄지손가락 감각이 안 돌아와. 지금도.”
64+
65+
무언가 비벼지는 소리가 들렸다. 비닐인지 옷인지 알 수 없었다. 나는 내 오른손을 폈다. 만두 봉지를 잡았던 손가락 끝에 성에 녹은 물이 묻어 있었다.
66+
67+
“미래에서 전화할 수 있으면 로또 번호를 알려 줘야 하는 거 아니야?”
68+
69+
“오늘 화요일이야. 추첨 안 했어.”
70+
71+
내 방 달력은 월요일이었다.
72+
73+
“내일 딱 하루만 보여?”
74+
75+
“보이는 게 아니라, 내가 여기 있다고.”
76+
77+
상대가 처음으로 짜증을 냈다. 그 말투는 익숙했다. 내가 고객센터에서 같은 설명을 세 번째 할 때 내는 목소리였다.
78+
79+
통화 화면을 봤다. 숫자가 이상했다. 통화 시간이 늘어나는 대신 줄어들고 있었다.
80+
81+
00:38.
82+
83+
00:37.
84+
85+
“이거 시간이 왜 이래?”
86+
87+
“3분이 다야. 11시 20분 되면 끊겨. 다시 걸어도 그날은 안 돼.”
88+
89+
“통신사에 알아봤어?”
90+
91+
“제발. 왼쪽으로 가겠다고만 해.”
92+
93+
나는 잠깐 생각했다. 손해 볼 일은 없었다. 길 건너편으로 걷는 데 무슨 대단한 결심이 필요한 것도 아니었다.
94+
95+
“알았어. 왼쪽으로 갈게.”
96+
97+
상대가 길게 숨을 내쉬었다.
98+
99+
그 뒤로 이상할 만큼 조용해졌다. 조금 전까지 들리던 낮은 웅웅거림이 사라졌다. 냉장고나 환풍기 같은 소리였는데, 꺼지고 나서야 있었다는 걸 알았다.
100+
101+
“잠깐.”
102+
103+
상대가 말했다.
104+
105+
“왜 이러지.”
106+
107+
“뭐가.”
108+
109+
“밖에 좀…….”
110+
111+
00:02.
112+
113+
의자가 넘어졌다.
114+
115+
00:01.
116+
117+
“야, 가지 마.”
118+
119+
전화가 끊겼다.
120+
121+
나는 한동안 휴대폰을 귀에 붙이고 있었다. 다시 전화를 걸자 통화 중이라는 안내가 나왔다. 두 번 더 걸어도 같았다.
122+
123+
만두는 결국 먹지 않았다. 냉동실에 돌려놓으면서 봉지 집게를 찾았는데, 오른손으로 서랍을 뒤지는 일이 어쩐지 불편했다.
124+
125+
***
126+
127+
다음 날, 나는 평소와 같은 시간에 집을 나왔다.
128+
129+
편의점 앞까지 가는 길은 오른쪽 인도가 그늘이었다. 왼쪽은 햇빛이 정면으로 들어왔다. 나는 횡단보도 앞에서 신호 한 번을 더 기다렸다. 고작 그 일 때문에 지각하게 생겼다.
130+
131+
8시 41분.
132+
133+
반대편에서 편의점 직원이 생수 상자를 옮기고 있었다. 파란 천막도, 탑차도 있었다. 그 편의점에는 거의 매일 같은 차가 왔다. 누군가 내 출근길을 알고 있으면 할 수 있는 말이었다.
134+
135+
나는 휴대폰으로 차를 찍었다. 사진을 찍는 동안에는 내가 멍청한 짓을 한다는 느낌이 조금 덜했다.
136+
137+
기사가 운전석 문을 닫았다.
138+
139+
짧은 후진 경고음이 두 번 울리고, 철판을 주먹으로 치는 듯한 소리가 났다. 차 뒤쪽이 천막 기둥을 밀었다. 기둥은 생각보다 쉽게 꺾였다. 천막 아래 세워 둔 입간판이 넘어지면서 바닥을 긁었다.
140+
141+
내가 늘 지나가는 자리였다.
142+
143+
직원은 생수 상자를 놓친 채 소리를 질렀다. 기사는 차에서 내려 기둥과 범퍼를 번갈아 봤다. 다친 사람은 없었다.
144+
145+
휴대폰 위에 8시 42분이 떠 있었다.
146+
147+
나는 오른손으로 왼쪽 손목을 잡았다가, 반대로 잡았다.
148+
149+
회사에는 배탈이 났다고 했다.
150+
151+
***
152+
153+
그날 밤에는 11시부터 식탁에 앉아 있었다.
154+
155+
휴대폰 배터리는 100퍼센트였다. 충전선을 뺐다가 다시 꽂았다. 어제의 통화 기록은 남아 있었지만, 번호를 눌러 보면 언제나 통화 중이었다.
156+
157+
나는 메모장에 물어볼 것을 적었다.
158+
159+
어떻게 거는지. 어디서 알았는지. 어제 마지막에 무슨 일이 있었는지.
160+
161+
11시 16분이 되자 메모장을 닫았다. 화면을 다른 것으로 채워 두면 전화를 놓칠 것 같았다.
162+
163+
정각에 울렸다.
164+
165+
“왼쪽으로 갔어.”
166+
167+
나는 여보세요 대신 말했다.
168+
169+
“알아.”
170+
171+
목소리가 달랐다. 같은 내 목소리였지만 코가 막히지 않았다. 종이 넘기는 소리가 났다.
172+
173+
“다행이다. 손은 어때?”
174+
175+
“무슨 손?”
176+
177+
“오른손. 어제 감각 없다고 했잖아.”
178+
179+
상대가 잠시 조용해졌다.
180+
181+
“아. 그 전화.”
182+
183+
“그 전화라니.”
184+
185+
“나도 받았지. 그래서 왼쪽으로 갔고.”
186+
187+
나는 식탁에 펼쳐 놓은 메모를 봤다. 첫 질문 옆에 아무것도 쓸 수 없었다.
188+
189+
“그럼 어제 나한테 전화한 건 누구야.”
190+
191+
“너.”
192+
193+
“그건 아는데, 걔는 다쳤다며.”
194+
195+
“네가 오늘 안 다쳤잖아.”
196+
197+
상대는 당연한 걸 묻는다는 듯 말했다.
198+
199+
“그런 내일은 없어진 거지.”
200+
201+
나는 펜을 들었다. 손목이 멀쩡했다. 손가락도, 손가락으로 누른 종이도 아주 선명했다. 이상한 일인 건 분명했지만 결과는 좋았다. 물어볼 일이 많아도 순서를 정하면 될 것 같았다.
202+
203+
“그럼 지금은 수요일이고?”
204+
205+
“응. 내일 회의 때 부장이 일정 물어보면, 금요일이라고 하지 말고 다음 주 화요일이라고 해. 이유는 내가 설명…….”
206+
207+
상대의 말 뒤로 다른 목소리가 겹쳤다.
208+
209+
처음에는 텔레비전인 줄 알았다. 아주 작았고, 멀리서 들렸다.
210+
211+
“여기.”
212+
213+
내 펜이 멈췄다.
214+
215+
코가 막힌 사람처럼 입으로 짧게 숨을 들이마시는 소리.
216+
217+
“나 아직 여기 있어.”

0 commit comments

Comments
 (0)