-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathspec.yaml
More file actions
614 lines (603 loc) · 33.5 KB
/
Copy pathspec.yaml
File metadata and controls
614 lines (603 loc) · 33.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
product:
name: "feedme"
description: "URL에서 콘텐츠를 마크다운으로 추출하여 AI 챗봇에 붙여넣을 수 있는 미니멀 웹 도구"
features:
- id: feedme-extract
title: "콘텐츠 추출"
scenarios:
- id: FEEDME-001
given: "URL 입력창이 비어 있는 초기 화면"
when: "URL 입력란에 웹페이지 URL을 입력하고 '추출' 버튼을 클릭한다"
then: "제목, 출처, 본문이 렌더링된 마크다운 미리보기로 표시된다"
examples:
- input: { url: "https://example.com/article" }
expect: { previewVisible: true, previewContainsTitle: true, previewContainsSource: true, previewContainsBody: true }
- id: FEEDME-003
given: "유효한 URL을 입력하고 '가져오기' 버튼을 클릭한 직후"
when: "서버에서 콘텐츠를 가져오는 동안"
then: "버튼에 스피너 아이콘만 표시되고 버튼이 disabled된다"
examples:
- input: { url: "https://example.com/article" }
expect: { buttonSpinner: true, buttonDisabled: true, buttonTextVisible: false, separateLoadingArea: false }
excludes:
- "콘텐츠 편집 기능 — 설계 원칙 '편집 없이 바로 복사'"
- "추출 히스토리 저장 — MVP 범위 외"
- id: feedme-error
title: "에러 처리"
scenarios:
- id: FEEDME-006
given: "URL 입력란이 비어 있는 상태"
when: "화면을 확인한다"
then: "'가져오기' 버튼이 disabled 상태로 표시된다"
examples:
- input: { url: "" }
expect: { buttonDisabled: true }
- id: FEEDME-007
given: "URL 입력란에 잘못된 형식의 텍스트가 입력된 상태"
when: "화면을 확인한다"
then: "'가져오기' 버튼이 disabled 상태로 표시된다"
examples:
- input: { url: "not-a-url" }
expect: { buttonDisabled: true }
- id: FEEDME-008
given: "URL 입력란에 접근 불가능한 URL이 입력된 상태"
when: "'가져오기' 버튼을 클릭한다"
then: "입력 필드 하단에 '페이지에 접근할 수 없습니다' 인라인 에러 텍스트가 빨간색으로 표시된다"
examples:
- input: { url: "https://unreachable.invalid" }
expect: { inlineErrorText: "페이지에 접근할 수 없습니다", alertComponentVisible: false }
- id: FEEDME-011
given: "네트워크 연결 오류 또는 서버 응답 없음 상태에서 유효한 URL이 입력된 상태"
when: "'가져오기' 버튼을 클릭한다"
then: "입력 필드 하단에 '네트워크 오류가 발생했습니다' 인라인 에러 텍스트가 빨간색으로 표시된다"
examples:
- input: { url: "https://example.com/article", networkOffline: true }
expect: { inlineErrorText: "네트워크 오류가 발생했습니다", alertComponentVisible: false }
- id: FEEDME-012
given: "URL 입력란에 잘못된 형식의 텍스트가 입력되어 버튼이 비활성화된 상태"
when: "유효한 URL('https://example.com')로 수정한다"
then: "'가져오기' 버튼이 활성화된다"
examples:
- input: { initialUrl: "not-a-url", updatedUrl: "https://example.com" }
expect: { buttonDisabled: false }
- id: FEEDME-010
given: "서버 에러로 인라인 에러 텍스트가 표시된 상태"
when: "사용자가 URL 입력란을 수정한다"
then: "인라인 에러 텍스트가 즉시 사라진다"
examples:
- input: { currentError: "페이지에 접근할 수 없습니다", userTypesInInput: true }
expect: { inlineErrorText: null }
- id: improve-ui
title: "마크다운 UI 개선"
scenarios:
- id: FEEDME-020
given: "긴 콘텐츠가 포함된 웹페이지 URL을 추출하여 미리보기가 표시된 상태"
when: "미리보기 영역을 확인한다"
then: "높이 제한 없이 전체 콘텐츠가 페이지에 펼쳐져 표시된다"
examples:
- input: { contentLength: "long" }
expect: { hasMaxHeight: false, allContentVisible: true }
- id: FEEDME-022
given: "콘텐츠가 추출되어 미리보기가 표시된 상태"
when: "미리보기 영역의 스타일을 확인한다"
then: "미리보기 영역에 테두리(border)가 없다"
examples:
- input: { contentType: "webpage" }
expect: { hasBorder: false }
- id: FEEDME-023
given: "feedme 페이지에 접속한 상태"
when: "콘텐츠 영역의 너비를 확인한다"
then: "콘텐츠 영역의 최대 너비가 672px(max-w-2xl) 이상이다"
examples:
- input: { page: "feedme" }
expect: { maxWidth: "max-w-2xl" }
- id: FEEDME-024
given: "취소선(~~text~~)이 포함된 마크다운이 추출된 상태"
when: "미리보기 영역을 확인한다"
then: "취소선이 적용된 텍스트(<del>)로 렌더링된다"
examples:
- input: { markdown: "~~취소선 텍스트~~" }
expect: { renderedElement: "del", textContent: "취소선 텍스트" }
- id: FEEDME-025
given: "마크다운 테이블이 포함된 콘텐츠가 추출된 상태"
when: "미리보기 영역을 확인한다"
then: "HTML <table>로 렌더링된다"
examples:
- input: { markdown: "| 헤더1 | 헤더2 |\n|---|---|\n| 값1 | 값2 |" }
expect: { renderedElement: "table", cellText: "값1" }
- id: FEEDME-026
given: "태스크리스트(- [x] 완료)가 포함된 마크다운이 추출된 상태"
when: "미리보기 영역을 확인한다"
then: "체크박스가 포함된 리스트로 렌더링된다"
examples:
- input: { markdown: "- [x] 완료\n- [ ] 미완료" }
expect: { checkedCheckbox: true, uncheckedCheckbox: true }
- id: FEEDME-027
given: "언어가 지정된 코드 블록(```javascript)이 포함된 마크다운이 추출된 상태"
when: "미리보기 영역을 확인한다"
then: "코드 블록 내 키워드가 색상으로 구분되어 표시된다"
examples:
- input: { markdown: "```javascript\nconst x = 1;\n```" }
expect: { hasHighlightedCode: true, languageClass: "language-javascript" }
- id: FEEDME-028
given: "자동 링크(https://example.com 형태의 URL)가 포함된 마크다운이 추출된 상태"
when: "미리보기 영역을 확인한다"
then: "URL 텍스트가 클릭 가능한 <a> 링크로 렌더링된다"
examples:
- input: { markdown: "방문하세요 https://example.com 여기에" }
expect: { renderedElement: "a", href: "https://example.com" }
- id: FEEDME-029
given: "언어가 지정되지 않은 코드 블록이 포함된 마크다운이 추출된 상태"
when: "미리보기 영역을 확인한다"
then: "하이라이팅 없이 기본 코드 블록 스타일로 표시된다"
examples:
- input: { markdown: "```\nplain text code\n```" }
expect: { hasHighlightedCode: false, renderedElement: "code" }
excludes:
- "마크다운/원문 토글 기능 — '편집 없이 바로 복사' 원칙 유지"
- "복사 버튼 위치 변경 — 현재 위치 유지"
- "다크 모드별 하이라이팅 테마 — dark-mode 피처에서 처리"
preconditions:
- "remark-gfm 패키지 설치"
- "rehype-highlight 패키지 설치"
- id: rate-limit
title: "Rate Limit"
scenarios:
- id: RATE-001
given: "오늘 추출 요청을 보내지 않은 상태"
when: "URL 입력란에 URL을 입력하고 '가져오기' 버튼을 클릭한다"
then: "콘텐츠가 정상적으로 추출되어 표시된다"
examples:
- input: { url: "https://example.com/article", dailyRequestCount: 0 }
expect: { httpStatus: 200, previewVisible: true }
- input: { url: "https://example.com/article", dailyRequestCount: 99 }
expect: { httpStatus: 200, previewVisible: true }
- id: RATE-002
given: "오늘 동일 IP에서 100회 요청을 보낸 상태"
when: "URL 입력란에 URL을 입력하고 '가져오기' 버튼을 클릭한다"
then: "입력 필드 하단에 '요청 한도를 초과했습니다. 내일 다시 시도해주세요' 인라인 에러 텍스트가 빨간색으로 표시된다"
examples:
- input: { url: "https://example.com/article", dailyRequestCount: 100 }
expect: { httpStatus: 429, inlineErrorText: "요청 한도를 초과했습니다. 내일 다시 시도해주세요" }
- id: RATE-003
given: "어제 동일 IP에서 100회 요청을 모두 소진한 상태에서 날짜가 변경된 시점 (UTC 기준)"
when: "URL 입력란에 URL을 입력하고 '가져오기' 버튼을 클릭한다"
then: "콘텐츠가 정상적으로 추출되어 표시된다"
examples:
- input: { url: "https://example.com/article", yesterdayRequestCount: 100, todayRequestCount: 0 }
expect: { httpStatus: 200, previewVisible: true }
- id: RATE-004
given: "IP-A가 오늘 100회 요청을 모두 소진한 상태"
when: "IP-B에서 URL 입력란에 URL을 입력하고 '가져오기' 버튼을 클릭한다"
then: "IP-B의 콘텐츠가 정상적으로 추출되어 표시된다"
examples:
- input: { url: "https://example.com/article", ipA_dailyRequestCount: 100, ipB_dailyRequestCount: 0 }
expect: { httpStatus: 200, previewVisible: true }
excludes:
- "Burst 방지 (분 단위 제한) — 일 100회 총량 제한으로 충분"
- "글로벌 rate limit — IP 기반만 적용"
- "런타임 설정 변경 — 코드 상수로 관리"
preconditions:
- "Cloudflare Worker 추출 엔드포인트가 구현되어 있다"
- "Cloudflare KV 네임스페이스가 바인딩되어 있다"
- id: upgrade-logo
title: "로고 개선"
scenarios:
- id: LOGO-001
given: "feedme 페이지에 접속한 상태"
when: "로고 영역을 확인한다"
then: "HyperText 컴포넌트로 'Feed-me' 텍스트가 스크램블 애니메이션과 함께 표시된다"
examples:
- input: { page: "feedme" }
expect: { logoText: "Feed-me", hasHyperTextComponent: true }
- id: LOGO-002
given: "feedme 페이지에 접속한 상태"
when: "로고 위에 마우스를 올린다"
then: "스크램블 애니메이션이 재실행되고 포인터 커서가 표시된다"
examples:
- input: { action: "hoverLogo" }
expect: { cursorStyle: "pointer", animationRetriggered: true }
- id: LOGO-003
given: "URL 입력란에 'https://example.com'이 입력되고 콘텐츠가 추출되어 미리보기가 표시된 상태"
when: "로고 'Feed-me'를 클릭한다"
then: "URL 입력값이 비워지고, 추출 결과가 사라지고, 초기 화면으로 돌아간다"
examples:
- input: { currentUrl: "https://example.com", hasResult: true, action: "clickLogo" }
expect: { urlInputValue: "", previewVisible: false, errorVisible: false, copyButtonVisible: false }
- id: LOGO-004
given: "에러 메시지가 표시된 상태"
when: "로고 'Feed-me'를 클릭한다"
then: "에러 메시지가 사라지고 초기 화면으로 돌아간다"
examples:
- input: { hasError: true, action: "clickLogo" }
expect: { urlInputValue: "", errorVisible: false, previewVisible: false }
excludes:
- "로고 폰트 변경 — 기존 font-bold 유지"
- "로고 사이즈 변경 — 기존 text-3xl 유지"
- "별도 로고 이미지/아이콘 — 텍스트 로고만 사용"
preconditions:
- "@magicui/hyper-text 컴포넌트 설치 완료"
- id: feedme-footer
title: "페이지 푸터"
scenarios:
- id: FEEDME-030
given: "feedme 페이지에 접속한 상태"
when: "페이지 하단을 확인한다"
then: "footer 영역에 'Feedback' 링크가 GitHub Issue 작성 페이지로 연결된다"
examples:
- input: { page: "feedme" }
expect: { feedbackLinkVisible: true, feedbackLinkHref: "https://github.com/toy-crane/feedme/issues/new" }
- id: FEEDME-031
given: "feedme 페이지에 접속한 상태"
when: "페이지 하단을 확인한다"
then: "footer 영역에 GitHub 아이콘 링크가 레포지토리 페이지로 연결된다"
examples:
- input: { page: "feedme" }
expect: { githubLinkVisible: true, githubLinkHref: "https://github.com/toy-crane/feedme" }
- id: FEEDME-032
given: "feedme 페이지에 접속한 상태"
when: "페이지 하단을 확인한다"
then: "footer 영역에 '© 2026 feed-me' 텍스트가 표시된다"
examples:
- input: { page: "feedme" }
expect: { copyrightText: "© 2026 feed-me" }
- id: FEEDME-033
given: "feedme 페이지에 접속한 상태"
when: "페이지 하단을 확인한다"
then: "footer 영역에 'by toycrane' 링크가 toycrane.xyz로 연결된다"
examples:
- input: { page: "feedme" }
expect: { toycraneText: "by toy-crane", toycraneLinkHref: "https://toycrane.xyz" }
excludes:
- "뉴스레터 구독 — MVP 범위 외"
- id: improve-copy-button
title: "복사 버튼 개선 (split button + dropdown)"
scenarios:
- id: FEEDME-040
given: "콘텐츠가 추출되어 미리보기가 표시된 상태"
when: "복사 버튼 영역을 확인한다"
then: "복사 아이콘과 '복사하기' 텍스트가 있는 메인 버튼과, 오른쪽에 chevron(▼) 아이콘 버튼이 함께 표시된다"
examples:
- input: { state: "contentExtracted" }
expect: { copyButtonVisible: true, copyButtonHasIcon: true, copyButtonText: "복사하기", chevronButtonVisible: true }
- id: FEEDME-041
given: "콘텐츠가 추출되어 split button이 표시된 상태"
when: "메인 '복사하기' 버튼을 클릭한다"
then: "마크다운이 클립보드에 복사되고, 버튼 아이콘이 체크로 바뀐다 (토스트 없음)"
examples:
- input: { action: "clickCopyButton" }
expect: { clipboardHasContent: true, copyButtonIcon: "check", toastVisible: false }
- id: FEEDME-042
given: "콘텐츠가 추출되어 split button이 표시된 상태"
when: "chevron(▼) 버튼을 클릭한다"
then: "드롭다운 메뉴에 '마크다운 다운로드', 'ChatGPT에서 열기', 'Claude에서 열기' 3개 항목이 각각 아이콘과 함께 표시된다"
examples:
- input: { action: "clickChevron" }
expect: { dropdownVisible: true, menuItems: ["마크다운 다운로드", "ChatGPT에서 열기", "Claude에서 열기"], menuItemCount: 3, menuItemsHaveIcons: true }
- id: FEEDME-046
given: "드롭다운 메뉴가 열린 상태"
when: "'마크다운 다운로드' 항목을 클릭한다"
then: "추출된 마크다운 내용이 페이지 제목 기반 파일명의 .md 파일로 다운로드된다"
examples:
- input: { action: "clickDownload", title: "Example Domain", markdown: "# Example Domain" }
expect: { downloadedFileName: "Example Domain.md", downloadedContent: "# Example Domain" }
- input: { action: "clickDownload", title: null, markdown: "# Hello" }
expect: { downloadedFileName: "feedme.md" }
- id: FEEDME-043
given: "드롭다운 메뉴가 열린 상태"
when: "'ChatGPT에서 열기' 항목을 클릭한다"
then: "마크다운 내용이 query에 포함된 ChatGPT URL이 새 탭에서 열린다"
examples:
- input: { action: "clickChatGPT", markdown: "# Hello" }
expect: { openedUrl: "https://chatgpt.com/?q=%23%20Hello", targetBlank: true }
- id: FEEDME-044
given: "드롭다운 메뉴가 열린 상태"
when: "'Claude에서 열기' 항목을 클릭한다"
then: "마크다운 내용이 query에 포함된 Claude URL이 새 탭에서 열린다"
examples:
- input: { action: "clickClaude", markdown: "# Hello" }
expect: { openedUrl: "https://claude.ai/new?q=%23%20Hello", targetBlank: true }
- id: FEEDME-045
given: "콘텐츠가 추출되지 않은 초기 화면"
when: "화면을 확인한다"
then: "split button이 표시되지 않는다"
examples:
- input: { state: "initial" }
expect: { splitButtonVisible: false }
excludes:
- "View as Markdown 옵션 — feedme가 이미 마크다운을 보여주므로 불필요"
- "v0, Scira 등 다른 서비스 — ChatGPT, Claude만 지원"
- "모바일 전용 Popover 분기 — DropdownMenu로 통일"
preconditions:
- "shadcn DropdownMenu 컴포넌트 설치"
- id: dark-mode
title: "다크 모드 토글"
scenarios:
- id: DARK-001
given: "feedme 페이지에 접속한 상태"
when: "화면 우측 상단을 확인한다"
then: "테마 전환 토글 버튼이 fixed 위치로 표시된다"
examples:
- input: { page: "feedme" }
expect: { themeToggleVisible: true, themeTogglePosition: "fixed" }
- id: DARK-002
given: "라이트 모드가 적용된 상태"
when: "테마 토글 버튼을 클릭한다"
then: "다크 모드로 전환되어 어두운 배경이 적용된다"
examples:
- input: { currentTheme: "light", action: "clickThemeToggle" }
expect: { htmlDarkClass: true, backgroundDark: true }
- id: DARK-003
given: "다크 모드가 적용된 상태"
when: "테마 토글 버튼을 클릭한다"
then: "라이트 모드로 전환되어 밝은 배경이 적용된다"
examples:
- input: { currentTheme: "dark", action: "clickThemeToggle" }
expect: { htmlDarkClass: false, backgroundDark: false }
- id: DARK-004
given: "OS 다크 모드가 설정되어 있고 feedme에 처음 방문한 상태"
when: "페이지가 로드된다"
then: "다크 모드로 표시된다"
examples:
- input: { osTheme: "dark", firstVisit: true }
expect: { htmlDarkClass: true, backgroundDark: true }
- input: { osTheme: "light", firstVisit: true }
expect: { htmlDarkClass: false, backgroundDark: false }
- id: DARK-005
given: "사용자가 토글로 다크 모드를 선택한 후 페이지를 새로고침한 상태"
when: "페이지가 로드된다"
then: "다크 모드가 유지된다"
examples:
- input: { savedTheme: "dark", action: "reload" }
expect: { htmlDarkClass: true, backgroundDark: true }
- input: { savedTheme: "light", action: "reload" }
expect: { htmlDarkClass: false, backgroundDark: false }
- id: DARK-006
given: "다크 모드에서 코드 블록이 포함된 마크다운이 표시된 상태"
when: "코드 블록 영역을 확인한다"
then: "다크 테마에 맞는 코드 하이라이팅이 적용된다"
examples:
- input: { currentTheme: "dark", markdown: "```javascript\nconst x = 1;\n```" }
expect: { darkHighlightTheme: true }
- id: DARK-007
given: "다크 모드에서 마크다운 콘텐츠가 추출되어 미리보기가 표시된 상태"
when: "마크다운 본문 영역을 확인한다"
then: "본문 텍스트가 밝은 색으로 표시되어 어두운 배경에서 읽을 수 있다"
examples:
- input: { currentTheme: "dark", markdown: "# 제목\n\n본문 텍스트입니다" }
expect: { proseInvertApplied: true, textReadableOnDarkBg: true }
- input: { currentTheme: "light", markdown: "# 제목\n\n본문 텍스트입니다" }
expect: { proseInvertApplied: false, textReadableOnLightBg: true }
excludes:
- "3단 모드(light/dark/system) 선택 UI — 토글로 2단 전환만 지원"
- "애니메이션 전환 효과 — MVP 범위 외"
- "커스텀 테마 색상 — 기존 CSS 변수 활용"
preconditions:
- "next-themes 패키지 설치 완료"
- "globals.css에 .dark CSS 변수 정의 완료"
- "ThemeProvider가 layout.tsx에 설정 완료"
- id: pre-prompt
title: "프리프롬프트"
scenarios:
- id: PROMPT-001
given: "콘텐츠가 추출되어 미리보기가 표시된 상태"
when: "제목과 복사 버튼 사이 영역을 확인한다"
then: "접힌 상태의 '프롬프트 추가하기' 토글이 표시된다"
examples:
- input: { state: "contentExtracted" }
expect: { collapsibleTriggerVisible: true, collapsibleOpen: false }
- id: PROMPT-002
given: "콘텐츠가 추출되지 않은 초기 화면"
when: "화면을 확인한다"
then: "'프롬프트 추가하기' 토글이 표시되지 않는다"
examples:
- input: { state: "initial" }
expect: { collapsibleTriggerVisible: false }
- id: PROMPT-003
given: "콘텐츠가 추출되어 '프롬프트 추가하기' 토글이 접힌 상태"
when: "'프롬프트 추가하기' 토글을 클릭한다"
then: "Textarea 입력창과 프리셋 칩 3개('요약해줘', '한국어로 번역해줘', '쉽게 설명해줘')가 표시된다"
examples:
- input: { action: "clickCollapsibleTrigger" }
expect: { textareaVisible: true, presetChips: ["요약해줘", "한국어로 번역해줘", "쉽게 설명해줘"], presetChipCount: 3 }
- id: PROMPT-004
given: "프롬프트 영역이 펼쳐진 상태"
when: "'프롬프트 추가하기' 토글을 클릭한다"
then: "Textarea와 프리셋 칩이 숨겨진다"
examples:
- input: { action: "clickCollapsibleTrigger", collapsibleOpen: true }
expect: { textareaVisible: false, presetChipsVisible: false }
- id: PROMPT-005
given: "프롬프트 영역이 펼쳐진 상태"
when: "'요약해줘' 프리셋 칩을 클릭한다"
then: "Textarea에 '요약해줘' 텍스트가 채워진다"
examples:
- input: { action: "clickPreset", preset: "요약해줘" }
expect: { textareaValue: "요약해줘", selectedPreset: "요약해줘" }
- input: { action: "clickPreset", preset: "한국어로 번역해줘" }
expect: { textareaValue: "한국어로 번역해줘", selectedPreset: "한국어로 번역해줘" }
- input: { action: "clickPreset", preset: "쉽게 설명해줘" }
expect: { textareaValue: "쉽게 설명해줘", selectedPreset: "쉽게 설명해줘" }
- id: PROMPT-006
given: "프리셋 '요약해줘'가 선택되어 Textarea에 채워진 상태"
when: "이미 선택된 '요약해줘' 칩을 다시 클릭한다"
then: "Textarea가 비워지고 칩 선택이 해제된다"
examples:
- input: { selectedPreset: "요약해줘", action: "clickPreset", preset: "요약해줘" }
expect: { textareaValue: "", selectedPreset: null }
- id: PROMPT-007
given: "프리셋 '요약해줘'가 선택되어 Textarea에 '요약해줘'가 채워진 상태"
when: "Textarea의 텍스트를 '요약해줘 한국어로'로 수정한다"
then: "ToggleGroup의 선택 상태가 해제된다"
examples:
- input: { selectedPreset: "요약해줘", action: "editTextarea", newValue: "요약해줘 한국어로" }
expect: { textareaValue: "요약해줘 한국어로", selectedPreset: null }
- input: { selectedPreset: null, action: "editTextarea", newValue: "직접 입력한 텍스트" }
expect: { textareaValue: "직접 입력한 텍스트", selectedPreset: null }
- id: PROMPT-008
given: "프롬프트에 '요약해줘'가 입력되고 마크다운 '# Hello'가 추출된 상태"
when: "'복사하기' 버튼을 클릭한다"
then: "'요약해줘\n\n# Hello'가 클립보드에 복사된다"
examples:
- input: { prompt: "요약해줘", markdown: "# Hello", action: "clickCopyButton" }
expect: { clipboardContent: "요약해줘\n\n# Hello" }
- input: { prompt: "", markdown: "# Hello", action: "clickCopyButton" }
expect: { clipboardContent: "# Hello" }
- id: PROMPT-009
given: "프롬프트에 '요약해줘'가 입력되고 마크다운이 추출된 상태"
when: "'ChatGPT에서 열기'를 클릭한다"
then: "'요약해줘\n\n마크다운'이 query에 인코딩된 ChatGPT URL이 새 탭에서 열린다"
examples:
- input: { prompt: "요약해줘", markdown: "# Hello", action: "clickChatGPT" }
expect: { openedUrlContains: "chatgpt.com/?q=", queryIncludesPrompt: true, queryIncludesMarkdown: true, targetBlank: true }
- input: { prompt: "요약해줘", markdown: "# Hello", action: "clickClaude" }
expect: { openedUrlContains: "claude.ai/new?q=", queryIncludesPrompt: true, queryIncludesMarkdown: true, targetBlank: true }
- input: { prompt: "", markdown: "# Hello", action: "clickChatGPT" }
expect: { openedUrlContains: "chatgpt.com/?q=", queryIncludesPrompt: false, queryIncludesMarkdown: true, targetBlank: true }
- id: PROMPT-010
given: "프롬프트에 '요약해줘'가 입력된 상태"
when: "'마크다운 다운로드'를 클릭한다"
then: "다운로드 파일에 마크다운만 포함되고 프롬프트는 포함되지 않는다"
examples:
- input: { prompt: "요약해줘", markdown: "# Hello", action: "clickDownload" }
expect: { downloadedContent: "# Hello", promptNotIncluded: true }
- id: PROMPT-011
given: "프롬프트에 '요약해줘'가 입력되고 Collapsible이 펼쳐진 상태"
when: "로고 'Feed-me'를 클릭한다"
then: "프롬프트가 비워지고, Collapsible이 접히고, ToggleGroup 선택이 해제된다"
examples:
- input: { prompt: "요약해줘", collapsibleOpen: true, action: "clickLogo" }
expect: { textareaValue: "", collapsibleOpen: false, selectedPreset: null }
- id: PROMPT-012
given: "프롬프트에 '요약해줘'가 입력된 상태"
when: "복사 버튼 영역을 확인한다"
then: "복사 버튼 아래에 '프롬프트 포함' 캡션이 표시된다"
examples:
- input: { prompt: "요약해줘" }
expect: { promptIncludedCaptionVisible: true }
- input: { prompt: "" }
expect: { promptIncludedCaptionVisible: false }
- id: PROMPT-013
given: "프롬프트에 '요약해줘'가 입력되고 Collapsible이 펼쳐진 상태"
when: "Textarea의 클리어(X) 버튼을 클릭한다"
then: "프롬프트가 비워지고 ToggleGroup 선택이 해제된다"
examples:
- input: { prompt: "요약해줘", action: "clickClearButton" }
expect: { textareaValue: "", selectedPreset: null }
- id: PROMPT-014
given: "프롬프트가 비어있는 상태"
when: "Textarea 영역을 확인한다"
then: "클리어(X) 버튼이 표시되지 않는다"
examples:
- input: { prompt: "" }
expect: { clearButtonVisible: false }
excludes:
- "프롬프트 히스토리 저장 — MVP 범위 외"
- "사용자 정의 프리셋 추가/삭제 — MVP 범위 외"
- "마크다운 다운로드 시 프롬프트 포함 — 다운로드는 원본 보존 목적"
preconditions:
- "shadcn Collapsible 컴포넌트 설치"
- "shadcn ToggleGroup 컴포넌트 설치"
- "Textarea 컴포넌트 이미 설치됨"
- id: align-ui
title: "결과 UI 통일"
scenarios:
- id: ALIGN-001
given: "OG 이미지가 있는 웹페이지 URL을 추출하여 결과가 표시된 상태"
when: "결과 영역을 확인한다"
then: "제목 위에 OG 이미지가 16:9 비율의 썸네일로 표시된다"
examples:
- input: { url: "https://example.com/article-with-og", hasOgImage: true }
expect: { thumbnailVisible: true, thumbnailAspectRatio: "16/9" }
- id: ALIGN-002
given: "OG 이미지가 없는 웹페이지 URL을 추출하여 결과가 표시된 상태"
when: "결과 영역을 확인한다"
then: "썸네일 영역이 표시되지 않는다"
examples:
- input: { url: "https://example.com/article-no-og", hasOgImage: false }
expect: { thumbnailVisible: false }
- id: ALIGN-003
given: "author 메타가 있는 웹페이지 URL을 추출하여 결과가 표시된 상태"
when: "제목 아래 영역을 확인한다"
then: "author 텍스트가 출처로 표시된다"
examples:
- input: { url: "https://example.com/article", author: "홍길동" }
expect: { sourceText: "홍길동" }
- id: ALIGN-004
given: "author 메타가 없는 웹페이지 URL을 추출하여 결과가 표시된 상태"
when: "제목 아래 영역을 확인한다"
then: "도메인명이 출처로 표시된다"
examples:
- input: { url: "https://blog.example.com/article", author: null }
expect: { sourceText: "blog.example.com" }
- id: ALIGN-005
given: "웹페이지 URL을 추출하여 결과가 표시된 상태"
when: "결과의 레이아웃 구조를 확인한다"
then: "동일한 요소 순서(제목 → 출처 → 복사 버튼 → 프롬프트 → 본문)와 동일한 간격으로 표시된다"
examples:
- input: { contentType: "webpage", hasOgImage: false }
expect: { elementOrder: ["title", "source", "copyButton", "prompt", "body"], gap: "gap-4" }
- id: ALIGN-006
given: "OG 이미지와 author 메타가 있는 웹페이지 URL로 추출 API를 호출한 상태"
when: "API 응답을 확인한다"
then: "응답에 thumbnail 필드와 source 필드가 포함된다"
examples:
- input: { url: "https://example.com/article-with-meta", hasOgImage: true, hasAuthor: true }
expect: { thumbnailInResponse: true, sourceInResponse: true }
- id: ALIGN-007
given: "author 메타가 없는 웹페이지 URL로 추출 API를 호출한 상태"
when: "API 응답을 확인한다"
then: "응답에 source 필드로 도메인명이 포함된다"
examples:
- input: { url: "https://blog.example.com/article", hasAuthor: false }
expect: { sourceInResponse: "blog.example.com" }
- id: ALIGN-009
given: "콘텐츠가 추출되어 결과가 표시된 상태"
when: "프롬프트 영역과 마크다운 본문 사이를 확인한다"
then: "프롬프트 영역과 본문 사이에 구분선(Separator)이 표시된다"
examples:
- input: { contentType: "webpage" }
expect: { separatorBetweenPromptAndBody: true }
excludes:
- "썸네일 크기/비율 변경 — 기존 16:9 유지"
- "웹페이지 favicon 표시 — MVP 범위 외"
preconditions:
- "Defuddle 라이브러리가 웹페이지에서 image, author를 파싱할 수 있다"
- id: fix-ui
title: "URL 입력창 Clear 버튼"
scenarios:
- id: FEEDME-050
given: "URL 입력창에 'https://example.com'이 입력된 상태"
when: "사용자가 입력창을 확인한다"
then: "입력창 오른쪽(fetch 버튼 왼쪽)에 X(clear) 버튼이 표시된다"
examples:
- input: { url: "https://example.com" }
expect: { clearButtonVisible: true }
- input: { url: "" }
expect: { clearButtonVisible: false }
- id: FEEDME-051
given: "URL 입력창에 'https://example.com'이 입력된 상태"
when: "X(clear) 버튼을 클릭한다"
then: "URL 입력창이 비워지고, 이미 불러온 결과(프리뷰)와 프롬프트는 유지된다"
examples:
- input: { url: "https://example.com", hasPreview: true }
expect: { urlField: "", previewVisible: true }
- id: FEEDME-052
given: "fetch가 진행 중인 상태"
when: "사용자가 입력창을 확인한다"
then: "X(clear) 버튼이 표시되지 않는다"
examples:
- input: { url: "https://example.com", loading: true }
expect: { clearButtonVisible: false }
- id: FEEDME-053
given: "에러 메시지가 표시된 상태에서 URL 입력창에 텍스트가 있는 상태"
when: "X(clear) 버튼을 클릭한다"
then: "URL 입력창이 비워지고 에러 메시지도 사라진다"
examples:
- input: { url: "https://bad-url.com", errorVisible: true }
expect: { urlField: "", errorVisible: false }
excludes:
- "전체 초기화 (결과, 프롬프트 등) — 로고 클릭으로 이미 제공됨"
- "키보드 단축키 (Escape 등) — 이번 범위 아님"
preconditions:
- "URL 입력 컴포넌트(UrlInputSection)가 구현되어 있다"