deel-local-cli 1.20.11 → 1.20.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -373,7 +373,23 @@ export const 표 = {
373
373
 
374
374
  ask: [
375
375
  // 설명은 아무것도 안 바꾸는 모드라 잘못 고르면 답답해진다. 신호를 엄격히 본다.
376
- [/뭐(야|예요|입니까|지)\s*\??$/m, 5], [/무엇(인가|입니까|이야)/, 5],
376
+ /*
377
+ * ── 물음표 **뒤에** 한마디를 더 붙이면 물음이 아니게 됐다 ─────────
378
+ *
379
+ * 줄 끝(`$`)에 못박혀 있었다. 그런데 사람은 물음표로 말을 안 끝낸다 —
380
+ *
381
+ * "이거 뭐야?" → ask 5점 → 설명 모드
382
+ * "이거 뭐야? 알려줘" → ask 3점 → 종합 모드
383
+ *
384
+ * 「알려줘」 를 붙였더니 물음이 아닌 것이 됐다. 붙인 말은 **더 설명해
385
+ * 달라는 말**인데 점수가 되레 깎였다(5 → 3). 남은 '알려 줘' 3점은
386
+ * 문턱 5에 모자라서 종합으로 떨어졌다.
387
+ *
388
+ * 물음표가 있으면 그 자리가 문장 끝이다. 줄 끝이 아니어도 된다.
389
+ * 물음표가 없을 때만 줄 끝을 따진다 — "뭐야 이게" 같은 말을 아무 데서나
390
+ * 줍지 않으려는 것이 원래 못의 뜻이었고, 그건 그대로 둔다.
391
+ */
392
+ [/뭐(야|예요|입니까|지)\s*(?:\?|$)/m, 5], [/무엇(인가|입니까|이야)/, 5],
377
393
  /*
378
394
  * 「뭐 하는 파일이야?」 — 터미널에서 돌려 보다 걸린 자리다.
379
395
  *
@@ -476,10 +492,46 @@ export const 표 = {
476
492
  * 문제가 아니라 **낱말 문제**다. 「말해 줘」 는 `explain` 과 같은 부류의
477
493
  * 물음 시킴말이라 그쪽에 넣는다 — 못을 뽑으면 위의 `Rewrite how to` 가
478
494
  * 도로 열린다.
495
+ *
496
+ * ── 공손한 한마디가 못 앞에 끼어들었다 ──────────────────────────────
497
+ *
498
+ * 첫머리 못을 `^\s*` 로 박아 놓으니 **빈칸 말고는 아무것도** 앞에 못
499
+ * 온다. 그런데 영어로 묻는 사람은 그 자리에 거의 항상 한마디를 붙인다 —
500
+ *
501
+ * "tell me what this does" → ask 5점
502
+ * "Please tell me what this does" → 전부 0점 → 종합 모드
503
+ * "Could you tell me what this does" → 전부 0점 → 종합 모드
504
+ *
505
+ * 공손하게 물을수록 물음으로 안 읽혔다. 답만 하면 되는 턴이 파일을
506
+ * 고칠 수 있는 모드로 돌았다는 뜻이다.
507
+ *
508
+ * 그래서 못 바로 뒤에 **공손말 한 마디**를 허락한다. 못을 뽑는 것이
509
+ * 아니다 — "Rewrite this function to show me the result." 은 여전히
510
+ * 0점이다. 그 자리의 `to show` 앞에는 공손말이 아니라 딴 말이 있다.
511
+ *
512
+ * 한 마디가 아니라 **두 마디**가 붙기도 한다. "Please could you tell
513
+ * me …" 는 실제로 흔한데, 세로줄로 갈라 놓으면 둘 중 하나만 먹고
514
+ * 나머지가 남아 통째로 0점이 됐다. 그래서 갈래가 아니라 차례로 둔다.
515
+ *
516
+ * `what` · `how` 쪽에는 `please` 만 준다. 「Could you what …」 은 영어에
517
+ * 없는 꼴이라 받아 봐야 걸릴 자리가 없다.
518
+ *
519
+ * ── 물어보고 **나서 고치라**는 한 문장 ───────────────────────────
520
+ *
521
+ * "Please show me the config and rename it to config.json."
522
+ * "Could you tell me what changed and update the changelog?"
523
+ *
524
+ * 공손말을 받아 주자 이것들이 5점을 꽉 채워 **묻기 모드**로 갔다. 묻기는
525
+ * 파일을 못 고치는 모드라, 사람은 설명만 받고 이름은 그대로다. 공손말을
526
+ * 받기 전에는 0점이라 종합(고칠 수 있는 자리)에 남아 있었으니, 이 변경이
527
+ * 없던 자리를 만든 셈이다.
528
+ *
529
+ * `and`·`then` 으로 이은 시킴 마디가 같은 문장에 있으면 이 규칙을 끈다.
530
+ * 「and why」 「and how it works」 처럼 움직씨가 아닌 것은 안 건드린다.
479
531
  */
480
- [/\bexplain(?:s|ing|ed)?\b|(?:^\s*|[.!?]\s+|\n\s*)(?:(?:tell|show)\s+(?:me|us)\b|walk\s+(?:me|us)\s+through\b)/im, 5],
481
- [/(?:^\s*|[.!?]\s+|\n\s*)what\b(?:(?:[^.!?\n]|\.(?=[\w/]))*\?|(?:'s|(?:\s+\w+){0,3}\s+(?:is|are|was|were|does|do|did|can|should)|\s+to)\b)/im, 5],
482
- [/(?:^\s*|[.!?]\s+|\n\s*)how\b(?:(?:[^.!?\n]|\.(?=[\w/]))*\?|'s\b|\s+(?:to\b|does|do|did|is|are|was|were|can|should)\b)/im, 5],
532
+ [/(?:\bexplain(?:s|ing|ed)?\b|(?:^\s*|[.!?]\s+|\n\s*)(?:(?:please|pls)\s*,?\s+)?(?:(?:could|can|would|will)\s+you\s+)?(?:please\s*,?\s+)?(?:(?:tell|show)\s+(?:me|us)\b|walk\s+(?:me|us)\s+through\b))(?![^.!?\n]*\b(?:and|then)\s+(?:also\s+)?(?:implement|add|fix|write|rename|update|delete|remove|create|build|make|refactor|rewrite)\b)/im, 5],
533
+ [/(?:^\s*|[.!?]\s+|\n\s*)(?:(?:please|pls)\s*,?\s+)?what\b(?:(?:[^.!?\n]|\.(?=[\w/]))*\?|(?:'s|(?:\s+\w+){0,3}\s+(?:is|are|was|were|does|do|did|can|should)|\s+to)\b)/im, 5],
534
+ [/(?:^\s*|[.!?]\s+|\n\s*)(?:(?:please|pls)\s*,?\s+)?how\b(?:(?:[^.!?\n]|\.(?=[\w/]))*\?|'s\b|\s+(?:to\b|does|do|did|is|are|was|were|can|should)\b)/im, 5],
483
535
  [/읽어(만)?\s*보고\s*(설명|알려)/, 4],
484
536
  ],
485
537
 
@@ -581,10 +633,53 @@ const 문턱 = {
581
633
  * 쪽이다. '고쳐줘' 한마디에 창이 뜨면 그게 더 나쁘다.
582
634
  * 그래서 **계획말 + 잇는말** 이 앞에 있고 그 **뒤에** 실행말이 와야만 겹침이다.
583
635
  */
584
- const 계획말 = '계획|플랜|설계|구상|기획|정리|검토|조사|파악|살펴|알아보|분석|로드맵|방향|순서|밑그림|초안';
585
- // '정리하고' 처럼 잇는 꼴이어야 한다. 그냥 '정리' 하나로는 겹침이 아니다 —
586
- // "코드 정리해줘" 는 계획이 아니라 그 자체가 할 일이다.
587
- const 잇는말 = '해\\s*주고|해\\s*놓고|하고\\s*나서|하고서|한\\s*다음|한\\s*뒤|한\\s*후|하고|해서|잡고|짜고|세우고|낸\\s*뒤|낸\\s*다음';
636
+ /*
637
+ * '알아보' 가 아니라 '알아' 다. 「알아보고」 는 '알아' + '보고' 로 갈라야
638
+ * 아래 잇는말의 '보고' 와 이어진다. '알아보' 로 묶어 두면 남는 것이 '고'
639
+ * 하나뿐이라 어떤 잇는말과도 안 맞았다 — 아래 머리말 참고.
640
+ */
641
+ const 계획말 = '계획|플랜|설계|구상|기획|정리|검토|조사|파악|살펴|알아|분석|로드맵|방향|순서|밑그림|초안';
642
+ /*
643
+ * '정리하고' 처럼 잇는 꼴이어야 한다. 그냥 '정리' 하나로는 겹침이 아니다 —
644
+ * "코드 정리해줘" 는 계획이 아니라 그 자체가 할 일이다.
645
+ *
646
+ * ── 한국말에서 가장 흔한 이음꼴이 빠져 있었다 ───────────────────────────
647
+ *
648
+ * 「…해 보고」 다. 「살펴보고」 「알아보고」 「검토해 보고」 — 계획을 먼저
649
+ * 하고 그다음에 하라는 말을 사람들이 실제로 제일 많이 쓰는 꼴인데, 목록에
650
+ * 없었다. 그래서 이 규칙이 잡으라고 만들어진 바로 그 문장이 안 잡혔다 —
651
+ *
652
+ * "살펴보고 만들어줘" → 겹침 아님 → code 모드 → 계획 한 줄 없이 파일부터
653
+ * "정리하고 만들어줘" → 겹침 → 계획을 보여 주고 승인을 받음
654
+ *
655
+ * 같은 말인데 이음꼴 하나로 갈렸다. 앞의 사람은 「먼저 보고 나서」 라고
656
+ * 적어 놓고도 계획을 못 봤다.
657
+ *
658
+ * ── 「보고」 를 이름씨와 가르는 자리 ────────────────────────────────────
659
+ *
660
+ * 처음엔 `보고(?![서를가])` 하나로 막았다. 「검토 보고서를 만들어줘」 는
661
+ * 겹침이 아니라 그냥 할 일이니 맞는 방향이었는데, **가르는 자리가 틀렸다.**
662
+ * 「살펴보고서 만들어줘」 의 '-고서' 는 이름씨가 아니라 멀쩡한 이음씨끝인데
663
+ * 같이 막혔다. 뒤에 뭐가 오느냐만 봐서는 둘을 못 가른다.
664
+ *
665
+ * 앞을 봐야 갈린다 — 그런데 「빈칸이 있나」 가 아니라 **앞말이 무엇이냐** 다.
666
+ *
667
+ * "살펴보고서 만들어줘" — 앞이 움직씨 줄기(살펴) → 이음말
668
+ * "검토보고서 작성해줘" — 앞이 이름씨(검토) → '보고서' 라는 한 낱말
669
+ *
670
+ * 처음엔 빈칸만 보고 갈랐다. 붙어 있으면 이음말로 쳤더니 「검토보고서」
671
+ * 「분석보고서」 가 통째로 겹침이 됐다 — 보고서를 써 달라는 말이 계획 모드로
672
+ * 가서 계획만 내고 멈춘다. 빈칸은 우연이고, 갈리는 것은 앞말의 품사다.
673
+ *
674
+ * '살펴·알아' 는 움직씨 줄기라 뒤에 오는 '보고' 가 언제나 도움움직씨다.
675
+ * '검토·분석·조사' 는 이름씨라 '보고' 가 붙으면 한 낱말이 된다. 이름씨 쪽은
676
+ * '해' 를 끼워야 도움움직씨가 된다 — 「검토해 보고」. 그 꼴에는 뒷말을
677
+ * 따진다(「검토해 보고서를」 은 다시 이름씨다).
678
+ */
679
+ const 이름씨아님 = '(?![서를가에의는도만까와과])';
680
+ const 잇는말 = '해\\s*주고|해\\s*놓고|하고\\s*나서|하고서|한\\s*다음|한\\s*뒤|한\\s*후|하고|해서|잡고|짜고|세우고|낸\\s*뒤|낸\\s*다음'
681
+ + `|(?<=(?:살펴|알아)\\s{0,1})보고|해\\s*보고${이름씨아님}`
682
+ + '|(?<=(?:살펴|알아)\\s{0,1})본\\s*(?:다음|뒤|후)|해\\s*본\\s*(?:다음|뒤|후)';
588
683
  const 실행말 = /만들어|만들자|만들어라|구현|작성|추가(해|하)|생성|써\s*(줘|주세요)|짜\s*(줘|주세요)|고쳐|세팅|설치|배포/;
589
684
  const 계획먼저 = new RegExp(`(${계획말})\\s*(?:을|를)?\\s*(?:${잇는말})`);
590
685
  /*
@@ -606,8 +701,47 @@ const 계획먼저 = new RegExp(`(${계획말})\\s*(?:을|를)?\\s*(?:${잇는
606
701
  *
607
702
  * 그래서 쉼표를 뺀다. 차례를 뜻하는 말이 있을 때만 겹침이다. 한국어 쪽은
608
703
  * 처음부터 잇는말 목록으로 이렇게 하고 있었고, 영어 쪽만 헐거웠다.
704
+ *
705
+ * ── 쉼표를 목록에서만 뺐지, 지나가는 길은 안 막았다 ─────────────────────
706
+ *
707
+ * 위를 그렇게 고쳐 놓고도 **바로 그 예문이 여전히 겹침으로 잡혔다.**
708
+ *
709
+ * "independently design, implement, test, and build the feature"
710
+ *
711
+ * 이음말 목록에서 `,` 를 뺀 것은 맞다. 그런데 목록에 남아 있던 맨 `and` 가
712
+ * `design` 에서 예순 자 떨어진 **맨 끝의 `, and build`** 와 짝지어졌다.
713
+ * 사이의 쉼표들은 `[\s\S]{0,60}?` 가 그냥 지나갔다. 열거를 차례로 읽는
714
+ * 결과는 처음과 똑같았다 — 계획 모드로 가서 계획만 내고 멈춘다.
715
+ *
716
+ * 그래서 길을 나눈다.
717
+ *
718
+ * - `and then` · `then` · `after that` · `before you` 처럼 **차례를 적어
719
+ * 둔 말**이면 사이에 쉼표가 있어도 겹침이다 ("Plan the migration,
720
+ * then implement it" — 사람이 차례라고 썼다).
721
+ * - 맨 `and` 하나로 잇는 것은 **쉼표를 하나까지만 넘는다** ("Design and
722
+ * implement" 과 "Plan the migration, and implement it" 은 겹침,
723
+ * "design, implement, test, and build" 는 열거).
724
+ *
725
+ * 쉼표 하나를 남겨 둔 까닭은, 영어에서 `and` 앞에 쉼표를 찍는 것이 그냥
726
+ * 흔해서다. 열거와 갈리는 자리는 **쉼표가 몇 개냐** 다 — 열거는 `and` 에
727
+ * 닿기까지 쉼표를 여러 번 넘어야 하고, 차례는 많아야 한 번이다.
728
+ *
729
+ * ── `the design` 은 계획을 세우라는 말이 아니다 ─────────────────────────
730
+ *
731
+ * 이 네 낱말은 움직씨이기도 하고 이름씨이기도 하다. 앞에 관사가 붙으면
732
+ * 이름씨다 — 그때는 계획을 세우라는 말이 아니라 그냥 무엇을 가리키는 말이다.
733
+ *
734
+ * "The design is fine. Run the linter and write the changelog."
735
+ * "Our plan is approved. Open the file and make the change."
736
+ *
737
+ * 둘 다 겹침으로 잡혀 계획 모드로 갔다. 뒤의 것은 사람이 **이미 승인됐다고
738
+ * 적어 놨는데** 또 계획을 내고 승인을 물었다.
739
+ *
740
+ * 맨 `and` 갈래는 문장 끝도 못 넘게 한다. 쉼표보다 센 구분자인데 `[^,]` 가
741
+ * 마침표·물음표·줄바꿈을 다 지나갔다. 그리고 `\s*` 는 상한이 없어서 예순 자
742
+ * 잣대가 빈칸으로 뚫렸다 — `[ \t]*` 로 좁힌다.
609
743
  */
610
- const 영어겹침 = /\b(plan|design|outline|draft)\b[\s\S]{0,60}?\b(?:and\s+then|then|and|after\s+(?:that|approval)|before\s+you)\s+(?:then\s+)?\b(build|implement|create|write|make|code)\b/i;
744
+ const 영어겹침 = /(?<!\b(?:the|a|an|this|that|our|your|my|its|their)\s)\b(plan|design|outline|draft)\b(?:[\s\S]{0,60}?\b(?:and\s+then|then|after\s+(?:that|approval)|before\s+you)|[^,.!?\n]{0,60}?,?[ \t]{0,2}\band)\s+(?:then\s+)?\b(build|implement|create|write|make|code)\b/i;
611
745
 
612
746
  /*
613
747
  * ── 두 말이 **얼마나 붙어 있어야** 겹침인가 ─────────────────────────────
@@ -740,7 +874,120 @@ export const 손대는동사 = '만들|고쳐|고치|바꿔|바꾸|추가|넣어
740
874
  * (`\\S{0,3}` 는 처음부터 두 개라 멀쩡했다. 한 무늬 안에서 한쪽만 틀렸으니
741
875
  * 눈으로는 더 안 보인다.)
742
876
  */
743
- const 손대라는말 = new RegExp(`(${손대는동사})\\S{0,3}(줘|주세요|주라|해라|하라|하자|해다오|해줄래|할래|어라|아라|여라|라(?=[\\s.!?~,]|$))`);
877
+ /*
878
+ * ── 띄어쓰기 한 칸이 시킴말을 통째로 숨겼다 ─────────────────────────────
879
+ *
880
+ * 사이를 `\S{0,3}` 로 적어서 **빈칸을 하나도 못 넘었다.** 그런데 「수정해 줘」
881
+ * 「고쳐 줘」 는 맞춤법대로 띄어 쓴 꼴이다. 그래서 같은 말이 이렇게 갈렸다 —
882
+ *
883
+ * "이 구조를 살펴보고 코드 수정해줘" → code 모드
884
+ * "이 구조를 살펴보고 코드 수정해 줘" → 종합 (손대라는 말이 아닌 것으로 읽힘)
885
+ *
886
+ * 모드만 갈린 것이 아니다. 이 검사는 「하라고 한 일을 두고 무슨 일을 할지
887
+ * 되묻지 않는다」 는 자리(askcheck.js)에도 쓰인다. 띄어 쓴 사람은 그 그물이
888
+ * 통째로 안 걸려서, 시켜 놓고도 「무슨 일을 할까요?」 를 다시 받았다.
889
+ *
890
+ * 세 글자 상한은 그대로다. 그래서 "나누는 설계만 봐줘" 는 여전히 안 걸린다 —
891
+ * 「나누」 와 「줘」 사이가 일곱 자다.
892
+ */
893
+ /*
894
+ * ── 「계획 정리해 줘」 는 계획을 다시 세워 달라는 말이다 ────────────────
895
+ *
896
+ * 영어 쪽에는 이 예외가 처음부터 있었다. `Update the plan.` 은 시킴말로 안
897
+ * 센다 — 고칠 파일 이름이 아니라 **계획 그 자체**가 목적어라서, 계획 모드로
898
+ * 가는 것이 맞다. 그런데 한국어 쪽에는 그 예외가 없었다.
899
+ *
900
+ * "이전 계획 정리해 줘" → 종합
901
+ * "Update the plan." → 계획
902
+ *
903
+ * 같은 말이 언어에 따라 다른 데로 갔다. 한국어로 물은 사람만 계획을 못 받았다.
904
+ *
905
+ * 목적어가 계획거리일 때만, 그리고 **계획을 손질하는 뜻의 동사**일 때만
906
+ * 뺀다. 영어가 그은 선과 같은 자리다 — `Implement the plan we agreed on.`
907
+ * 은 영어에서도 시킴말이다. 계획을 실행하라는 말은 손대라는 말이 맞다.
908
+ * 그래서 '구현·추가·삭제…' 는 그대로 두고 '만들·작성·바꾸·정리·수정' 만 뺀다.
909
+ *
910
+ * 「계획서」 「plan.md」 는 안 걸린다. 그건 계획거리가 아니라 **파일**이고,
911
+ * 파일을 고치라는 말은 고치라는 말이다. 영어가 `plan.md` 를 가르는 방법
912
+ * (`\.(?!\w)`)과 같은 뜻이다.
913
+ *
914
+ * 목록을 손대는동사 에서 갈라 내므로, 새 동사를 넣으면 자동으로 「예외 없는」
915
+ * 쪽에 들어간다 — 예외로 뺀 것은 '만들·작성·바꾸(바꿔)·정리·수정' 다섯 뜻이다.
916
+ *
917
+ * 계획거리와 동사 사이에 '좀' 이 끼는 것도 받는다. 「계획 좀 수정해 줘」 는
918
+ * 「계획 수정해 줘」 와 같은 말인데, 한 글자 때문에 딴 데로 가면 안 된다.
919
+ */
920
+ /*
921
+ * 계획거리는 plan 점수표에 있는 것만 적는다. '기획' 을 넣어 봤더니 예외는
922
+ * 걸리는데 갈 데가 없었다 — plan 점수가 0이라 「손대라는 말이 아니다」 까지만
923
+ * 가고 결국 종합이나 코드로 떨어졌다. 빼는 자리와 보내는 자리가 다르면
924
+ * 어느 쪽도 제 일을 못 한다.
925
+ *
926
+ * 사이에 끼는 말도 받는다. 「계획 다시 정리해 줘」 「계획 좀 더 정리해줘」 는
927
+ * 「계획 정리해 줘」 와 같은 말이다. 아무 말이나 받으면 「계획 파일 수정해 줘」
928
+ * 까지 빠지므로, 끼워도 되는 것은 **어찌씨 몇 개**로 못박는다.
929
+ */
930
+ const 계획거리 = '계획|플랜|로드맵';
931
+ const 끼는말 = '좀|더|다시|새로|전체|모두|싹|한번';
932
+ const 계획손질 = new Set(['만들', '작성', '바꿔', '바꾸', '정리', '수정']);
933
+ const 동사들 = 손대는동사.split('|');
934
+ const 계획도손대는동사 = 동사들.filter((v) => 계획손질.has(v)).join('|');
935
+ const 계획말고손대는동사 = 동사들.filter((v) => !계획손질.has(v)).join('|');
936
+ const 계획거리뒤 = `(?<!(?:${계획거리})[을를]?\\s{0,3}(?:(?:${끼는말})\\s{0,3}){0,2})`;
937
+ /*
938
+ * ── 빈칸을 넘어도 되는 어미와 안 되는 어미 ──────────────────────────────
939
+ *
940
+ * 「빈칸을 하나도 못 넘는다」 를 고치면서 사이를 `[^\n]{0,3}` 으로 열었다.
941
+ * 그런데 **어미마다 빈칸 사정이 다르다.**
942
+ *
943
+ * 빈칸을 넘어야 하는 것은 도움움직씨 '주다' 뿐이다 — 「고쳐 줘」 「고쳐
944
+ * 주세요」 는 맞춤법대로 띄어 쓴 꼴이다. '하자·해라·어라' 같은 씨끝은
945
+ * 줄기에서 절대 안 떨어진다.
946
+ *
947
+ * 그래서 전부에 빈칸을 열어 줬더니 이런 것이 걸렸다 —
948
+ *
949
+ * "추가 하자 있는 부분 알려줘" → 손대라는 말
950
+ *
951
+ * '하자' 가 씨끝이 아니라 **이름씨(결함)** 인데, 앞의 '추가' 와 빈칸으로만
952
+ * 떨어져 있어서 「추가하자」 로 읽혔다. 묻는 말이 고치라는 말이 됐다.
953
+ *
954
+ * ── 「고치지 마라」 는 고치라는 말이 아니다 ─────────────────────────────
955
+ *
956
+ * 이건 빈칸을 열기 **전부터** 있던 구멍이다(붙여 쓴 「고치지마라」 가 그때도
957
+ * 걸렸다). 사이가 세 글자라 '지 마' 가 통째로 들어앉고, 남은 '라' 가 시킴꼴
958
+ * 로 읽힌다. 하지 말라는 말이 하라는 말이 되는 것이라 뒤집힘이 제일 크다.
959
+ */
960
+ /*
961
+ * ── 맨 「라」 는 시킴꼴이기도 하고 서술격조사이기도 하다 ─────────────────
962
+ *
963
+ * "임시 파일 지워라" — 해라체 시킴꼴
964
+ * "이걸 개선이라 볼 수 있는지 검토해 줘" — 「개선이다」 의 '-이라'
965
+ *
966
+ * 뒤엣것이 시킴말로 읽혀서, 검토해 달라는 물음이 「고치라는 말」 이 됐다.
967
+ * 그러면 점검 모드가 후보에서 빠지고 화면에는 **거꾸로 된 까닭**이 뜬다 —
968
+ * 「고치라는 말이라 읽기 전용 모드로 안 보냄」. 고치라고 한 적이 없는데.
969
+ *
970
+ * 가르는 자리는 줄기의 종류다. '개선·구현·배포' 같은 한자어는 시킴꼴이
971
+ * 언제나 '…해라' 라 맨 '라' 가 붙을 일이 없다 — 붙었으면 그건 '-이라' 다.
972
+ * '지워·바꿔·고쳐' 같은 토박이 줄기만 맨 '라' 를 쓴다.
973
+ */
974
+ const 해붙는동사 = new Set(['추가', '삭제', '정리', '개선', '구현', '작성', '적용',
975
+ '설치', '배포', '리팩터링', '리팩터', '분리', '통일', '세팅', '수정']);
976
+ const 해붙는것 = 동사들.filter((v) => 해붙는동사.has(v)).join('|');
977
+ const 맨라 = `${해붙는것 ? `(?<!(?:${해붙는것})이?)` : ''}라(?=[\\s.!?~,]|$)`;
978
+ const 붙는끝 = `해라|하라|하자|해다오|할래|어라|아라|여라|${맨라}`;
979
+ const 띄는끝 = '줘|주세요|주라|줄래';
980
+ const 하지마 = '(?!\\S{0,2}지\\s*마|\\S{0,2}지\\s*말)';
981
+ /*
982
+ * 빈 갈래를 안 만든다. 위 두 목록은 손대는동사 를 갈라 낸 것이라, 목록을
983
+ * 손대다가 한쪽이 비면 `(?:)` 가 되고 **빈 글에 걸린다** — 그러면 모든 말이
984
+ * 손대라는 말이 되어 읽기 전용 모드가 통째로 사라진다. 아무 데서도 안 터지고
985
+ * 화면에도 안 나오는 꼴이라, 만들지 않는 쪽으로 막아 둔다.
986
+ */
987
+ const 손대라는말 = new RegExp(`(?:${[
988
+ 계획말고손대는동사 && `(?:${계획말고손대는동사})`,
989
+ 계획도손대는동사 && `${계획거리뒤}(?:${계획도손대는동사})`,
990
+ ].filter(Boolean).join('|')})${하지마}(?:\\S{0,3}(?:${붙는끝})|[^\\n]{0,3}(?:${띄는끝}))`);
744
991
 
745
992
  // 파일을 못 고치는 모드. modes.js 에서 쓰기 도구를 안 주는 것들과 같아야 한다.
746
993
  export const 읽기만하는모드 = new Set(['architect', 'plan', 'ask', 'inspect']);
@@ -43,14 +43,24 @@ export function runSessions(flags = {}) {
43
43
 
44
44
  const wId = Math.max(...rows.map((r) => width(r.id)));
45
45
  for (const [i, r] of rows.entries()) {
46
- const 최근 = i === 0 ? c.hgreen('●') : c.gray('·');
46
+ // 못 읽은 것도 목록에 올린다. 안 올리면 「없다」 가 되는데, 없는 것과
47
+ // 못 여는 것은 사람이 할 일이 다르다 (agent/store.js 의 list).
48
+ const 최근 = r.못읽음 ? c.yellow(mark.warn) : i === 0 ? c.hgreen('●') : c.gray('·');
47
49
  say(` ${최근} ${c.bold(pad(r.id, wId))} ${c.gray(pad(언제(r.at), 9))}`
48
- + `${c.gray(pad(`${r.turns}턴`, 6, 'right'))} ${c.gray(pad(clip(r.model, 22), 24))}`);
49
- say(` ${c.gray(' ')}${clip(r.first, 68)}`);
50
+ + `${c.gray(pad(r.못읽음 ? '' : `${r.turns}턴`, 6, 'right'))} ${c.gray(pad(clip(r.model, 22), 24))}`);
51
+ say(` ${c.gray(' ')}${r.못읽음 ? c.yellow(clip(r.first, 68)) : clip(r.first, 68)}`);
50
52
  }
51
53
  say('');
54
+ const 읽히는것 = rows.filter((r) => !r.못읽음);
55
+ if (!읽히는것.length) {
56
+ say(` ${mark.warn} ${c.yellow('여기 있는 대화를 하나도 못 열었습니다.')} ${c.gray('권한이나 다른 프로그램이 잡고 있는지 보세요.')}`);
57
+ say('');
58
+ say(` ${c.gray(`저장 위치: ${sessionsDir(root)} (.gitignore 에 들어 있어 깃에 안 올라갑니다)`)}`);
59
+ say('');
60
+ return 1;
61
+ }
52
62
  say(` ${c.gray('가장 최근 것 이어하기')} ${c.cyan('deel --continue')}`);
53
- say(` ${c.gray('골라서 이어하기')} ${c.cyan(`deel --resume ${rows[0].id}`)}`);
63
+ say(` ${c.gray('골라서 이어하기')} ${c.cyan(`deel --resume ${읽히는것[0].id}`)}`);
54
64
  say(` ${c.gray('하나 지우기')} ${c.cyan(`deel sessions --rm ${rows.at(-1).id}`)}`);
55
65
  say('');
56
66
  say(` ${c.gray(`저장 위치: ${sessionsDir(root)} (.gitignore 에 들어 있어 깃에 안 올라갑니다)`)}`);
@@ -14,7 +14,7 @@
14
14
  // 마지막 줄까지는 성하다. 통째로 다시 쓰는 방식이면 그 순간 파일이 깨진다.
15
15
  import {
16
16
  existsSync, mkdirSync, readdirSync, readFileSync,
17
- appendFileSync, writeFileSync, statSync, rmSync, chmodSync,
17
+ appendFileSync, writeFileSync, statSync, rmSync, chmodSync, renameSync,
18
18
  } from 'node:fs';
19
19
  import { join } from 'node:path';
20
20
 
@@ -41,7 +41,14 @@ export function freeId(dir, at = new Date()) {
41
41
  const id = `${base}-${n}`;
42
42
  if (!existsSync(join(dir, `${id}.jsonl`))) return id;
43
43
  }
44
- return `${base}-${process.pid}`;
44
+ // 여기까지 왔으면 마지막 이름도 비어 있는지 봐야 한다. 안 보고 돌려주면
45
+ // begin() 이 같은 이름으로 쉰 번을 되풀이하다 **아무 파일도 없이** 대화를
46
+ // 시작한다 — 저장한다고 적어 놓고 한 줄도 안 남는 판이 그때 만들어진다.
47
+ for (let n = 0; n < 1000; n++) {
48
+ const id = n ? `${base}-${process.pid}-${n}` : `${base}-${process.pid}`;
49
+ if (!existsSync(join(dir, `${id}.jsonl`))) return id;
50
+ }
51
+ return `${base}-${process.pid}-${Date.now().toString(36)}`;
45
52
  }
46
53
 
47
54
  export class Store {
@@ -86,6 +93,12 @@ export class Store {
86
93
  #잠그기() {
87
94
  if (this.#잠갔나) return;
88
95
  this.#잠갔나 = true;
96
+ /*
97
+ * 윈도우에서 chmod 는 **아무 일도 안 하고 성공한다.** 그 성공을 그대로
98
+ * 적으면 바로 위 머리말이 스스로 금지한 「잠근 척」 이 된다 — 같은 PC 를
99
+ * 여럿이 쓰는 사람이 화면만 보고 안심한다. 안 걸었으면 안 걸었다고 적는다.
100
+ */
101
+ if (process.platform === 'win32') { this.잠금 = { 못함: 'windows' }; return; }
89
102
  try { chmodSync(this.file, 0o600); this.잠금 = { 모드: 0o600 }; }
90
103
  catch (err) { this.잠금 = { 못함: err?.code ?? String(err) }; }
91
104
  }
@@ -96,8 +109,10 @@ export class Store {
96
109
  // 끊긴다 — 기록을 남기려다 작업을 죽이면 본말이 뒤집힌다.
97
110
  // 조용히 넘기는 것도 아니다: 폴더가 없으면 바로 아래 쓰기가 깨지고,
98
111
  // 그 자리에서 세어 화면에 오른다.
99
- try { mkdirSync(this.dir, { recursive: true }); } catch { /* 아래 쓰기가 센다 */ }
100
- this.opened = true;
112
+ // 못 만들었으면 **연 것이 아니다.** 연 것으로 적어 두면 잠깐 막혔던 것이
113
+ // 풀린 뒤에도 두 번 다시 안 만들어 본다 — 그 대화는 끝까지 한 줄도 안 남는다.
114
+ try { mkdirSync(this.dir, { recursive: true }); this.opened = true; }
115
+ catch { /* 아래 쓰기가 센다. 다음 줄에서 다시 만들어 본다 */ }
101
116
  }
102
117
 
103
118
  /*
@@ -161,14 +176,19 @@ export class Store {
161
176
  this.#use(freeId(this.dir)); // 누가 채 갔다. 옆자리로.
162
177
  }
163
178
  }
179
+ // 쉰 번을 다 써도 자리를 못 잡았다. 여기서 잠자코 돌아가면 화면은
180
+ // 「계속 저장되고 있습니다」 라고 말하는데 파일은 하나도 없다.
181
+ this.#못썼다({ code: 'EEXIST' });
164
182
  return this;
165
183
  }
166
184
 
185
+ /** 한 줄 적는다. 적었으면 true. 못 적었으면 false 이고 셈에 오른다. */
167
186
  #write(obj) {
168
187
  try {
169
188
  appendFileSync(this.file, JSON.stringify(obj) + '\n', 'utf8');
170
189
  this.#잠그기();
171
- } catch (err) { this.#못썼다(err); } // 계속은 하되, 몇 줄을 잃었는지는 센다
190
+ return true;
191
+ } catch (err) { this.#못썼다(err); return false; } // 계속은 하되, 몇 줄을 잃었는지는 센다
172
192
  }
173
193
 
174
194
  // 메시지 하나를 덧붙인다. 대화가 진행되는 대로 즉시 남긴다.
@@ -225,17 +245,24 @@ export class Store {
225
245
  * 그 값은 매번 사람이 기다리는 시간이다. pins 를 사람이 고칠 때만 적는
226
246
  * 것과 같은 뜻이다.
227
247
  */
248
+ /*
249
+ * 적은 것으로 치는 것은 **정말 적힌 뒤**다.
250
+ *
251
+ * 못 적었는데 적은 셈으로 표시해 두면, 디스크가 다시 나아져도 그 값은 이미
252
+ * 「안 바뀐 것」 이라 두 번 다시 안 적힌다. 잠깐 막혔던 것이 영영 사라지는
253
+ * 자리다 — 여기가 바로 남은 할 일과 시킨 말이 조용히 없어지던 길이다.
254
+ */
228
255
  살림적기(session = this.따라갈세션) {
229
256
  if (!session) return;
230
257
  const 할일 = JSON.stringify(session.할일 ?? []);
231
- if (할일 !== this.적은할일) {
258
+ if (할일 !== this.적은할일
259
+ && this.#write({ t: 'todo', at: new Date().toISOString(), 목록: session.할일 ?? [] })) {
232
260
  this.적은할일 = 할일;
233
- this.#write({ t: 'todo', at: new Date().toISOString(), 목록: session.할일 ?? [] });
234
261
  }
235
262
  const 요청 = String(session.이번요청 ?? '');
236
- if (요청 !== this.적은요청) {
263
+ if (요청 !== this.적은요청
264
+ && this.#write({ t: 'request', at: new Date().toISOString(), 글: 요청 })) {
237
265
  this.적은요청 = 요청;
238
- this.#write({ t: 'request', at: new Date().toISOString(), 글: 요청 });
239
266
  }
240
267
  }
241
268
 
@@ -289,15 +316,28 @@ export class Store {
289
316
  if (이번요청) lines.push(JSON.stringify({ t: 'request', at: new Date().toISOString(), 글: 이번요청 }));
290
317
  for (const m of messages) lines.push(JSON.stringify({ t: 'msg', m }));
291
318
  /*
292
- * 통째로 다시 쓰면 파일이 새로 만들어진다 — 그때 잠금도 다시 걸어야 한다.
293
- * 그리고 여기서 깨지면 앞의 append 들이 남긴 것까지 통째로 못 고친 셈이
294
- * 된다. 제일 크게 잃는 자리라 더더욱 조용히 넘길 수 없다.
319
+ * 옆에 다 쓰고 나서 **한 번에 갈아 끼운다.**
320
+ *
321
+ * 이 파일 머리말이 jsonl 을 고른 까닭이 「통째로 다시 쓰면 그 순간 파일이
322
+ * 깨진다」 인데, 정작 통째로 다시 쓰는 자리가 여기다. 디스크가 반쯤 쓰다
323
+ * 차면 원본은 이미 잘려 있고 새것은 안 끝났다 — 그 한 번에 대화 전체가
324
+ * 없어진다. 옆에 써 두고 이름만 바꾸면 성한 쪽 아니면 성한 쪽이다.
325
+ *
326
+ * 실패하면 옆에 쓴 것을 치운다. 안 치우면 폴더에 부스러기가 쌓이고,
327
+ * 다음에 볼 사람은 그게 대화인지 찌꺼기인지 모른다.
295
328
  */
329
+ const 옆 = `${this.file}.새로`;
296
330
  try {
297
- writeFileSync(this.file, lines.join('\n') + '\n', 'utf8');
331
+ writeFileSync(옆, lines.join('\n') + '\n', 'utf8');
332
+ // 윈도우에서는 읽기 전용 파일 위로 이름을 못 바꾼다. 우리 파일이니 푼다.
333
+ try { chmodSync(this.file, 0o600); } catch { /* 없거나 이미 쓸 수 있다 */ }
334
+ renameSync(옆, this.file);
298
335
  this.#잠갔나 = false;
299
336
  this.#잠그기();
300
- } catch (err) { this.#못썼다(err); }
337
+ } catch (err) {
338
+ try { rmSync(옆, { force: true }); } catch { /* 못 치워도 원본은 성하다 */ }
339
+ this.#못썼다(err);
340
+ }
301
341
  }
302
342
 
303
343
  readMeta() {
@@ -343,8 +383,16 @@ export class Store {
343
383
 
344
384
  // 사람에게 보여줄 첫 마디. 이게 목록에서 대화를 알아보는 유일한 단서다.
345
385
  function firstAsk(messages) {
346
- const m = messages.find((x) => x.role === 'user' && typeof x.content === 'string'
347
- && x.content.trim() && !x.content.startsWith('['));
386
+ const 쓸만한 = messages.filter((x) => x.role === 'user' && typeof x.content === 'string' && x.content.trim());
387
+ /*
388
+ * 대괄호로 여는 것은 우리가 끼워 넣은 글이라 건너뛴다. 그런데 사람도
389
+ * 「[급함] 로그인이 안 됩니다」 처럼 적는다 — 그 대화는 목록에서 「(빈
390
+ * 대화)」 가 됐다. 있는 대화를 없다고 적은 것이다.
391
+ *
392
+ * 그래서 건너뛰되, 남는 것이 하나도 없으면 첫 마디를 그대로 쓴다.
393
+ * 알아보기 어려운 제목이 「빈 대화」 라는 거짓말보다 낫다.
394
+ */
395
+ const m = 쓸만한.find((x) => !x.content.trim().startsWith('[')) ?? 쓸만한[0];
348
396
  return m ? m.content.replace(/\s+/g, ' ').trim() : '(빈 대화)';
349
397
  }
350
398
 
@@ -387,29 +435,73 @@ export function list(root, { limit = 20, 속까지 = true } = {}) {
387
435
  const out = [];
388
436
  for (const c of 후보) {
389
437
  if (out.length >= limit) break;
390
- const { meta, messages } = new Store(root, c.id).load();
391
- if (!messages.length) continue; // 빈 파일은 목록에 안 올린다
392
- out.push({
393
- ...c,
394
- model: meta?.model ?? '?',
395
- turns: messages.filter((m) => m.role === 'user').length,
396
- messages: messages.length,
397
- first: firstAsk(messages),
398
- });
438
+ const 줄 = 한줄(root, c);
439
+ if (줄) out.push(줄);
399
440
  }
400
441
  return out;
401
442
  }
402
443
 
444
+ /**
445
+ * 후보 하나를 열어 목록 한 줄로 만든다. 빈 파일이면 null.
446
+ *
447
+ * **못 읽은 것은 없는 것이 아니다.**
448
+ *
449
+ * load() 는 까닭을 담아 돌려주는데 부르는 쪽이 그것을 버리고 있었다. 그래서
450
+ * 권한이 막히거나 다른 프로그램이 잡고 있는 대화는 목록에서 통째로 사라졌고,
451
+ * 그런 것뿐이면 화면은 「아직 없습니다. 지금 이 대화가 첫 번째입니다」 라고
452
+ * 말했다 — 어제 한 일이 그대로 있는데 없다고 한 것이다. 이어할 수는 없어도
453
+ * **거기 있다는 것**은 말해야 사람이 손을 쓴다.
454
+ */
455
+ function 한줄(root, c) {
456
+ const { meta, messages, 못읽음 } = new Store(root, c.id).load();
457
+ if (못읽음) {
458
+ return { ...c, 못읽음, model: '?', turns: 0, messages: 0, first: `못 읽었습니다 — ${못읽음}` };
459
+ }
460
+ if (!messages.length) return null; // 빈 파일은 목록에 안 올린다
461
+ return {
462
+ ...c,
463
+ model: meta?.model ?? '?',
464
+ turns: messages.filter((m) => m.role === 'user').length,
465
+ messages: messages.length,
466
+ first: firstAsk(messages),
467
+ };
468
+ }
469
+
403
470
  export function remove(root, id) {
471
+ /*
472
+ * 이름에 상위 경로가 섞이면 대화 폴더 **밖**을 지운다.
473
+ * `deel sessions --rm ../../어딘가` 는 화면에 「지웠습니다」 라고 적고
474
+ * 엉뚱한 파일을 없앤다 — 무엇을 지웠는지 사람이 못 알아본다.
475
+ */
476
+ if (!/^[^\\/:*?"<>|]+$/.test(String(id)) || String(id).includes('..')) {
477
+ return { error: `대화 이름이 아닙니다: ${id}` };
478
+ }
404
479
  const f = join(sessionsDir(root), `${id}.jsonl`);
405
480
  if (!existsSync(f)) return { error: `그런 대화가 없습니다: ${id}` };
406
- rmSync(f, { force: true });
481
+ try { rmSync(f, { force: true }); }
482
+ catch (err) { return { error: `못 지웠습니다: ${id} — ${err?.code ?? err?.message ?? String(err)}` }; }
407
483
  return { removed: id };
408
484
  }
409
485
 
410
- /** 가장 최근 대화. --continue 가 집어 오는 것. */
486
+ /**
487
+ * 가장 최근 대화. --continue 가 집어 오는 것.
488
+ *
489
+ * 못 읽는 것은 건너뛴다. 그것을 집어 오면 --continue 가 **빈 대화**로 열리고,
490
+ * 사람은 어제 하던 것이 이어진 줄 알고 말을 건다.
491
+ */
411
492
  export function latest(root) {
412
- return list(root, { limit: 1 })[0] ?? null;
493
+ /*
494
+ * **한 개씩 열어 보고 첫 성한 것에서 멈춘다.**
495
+ *
496
+ * `list(root, { limit: 20 })` 로 스무 개를 통째로 읽으면 안 된다 — 이 파일이
497
+ * 위에서 경고한 그 자리다. 대화 파일 하나가 수 MB 이고, 이건 켤 때마다 돈다.
498
+ * 보통은 첫 파일 하나만 읽고 끝난다.
499
+ */
500
+ for (const c of list(root, { limit: 20, 속까지: false })) {
501
+ const 줄 = 한줄(root, c);
502
+ if (줄 && !줄.못읽음) return 줄;
503
+ }
504
+ return null;
413
505
  }
414
506
 
415
507
  /**
@@ -417,10 +509,18 @@ export function latest(root) {
417
509
  * 최근 keep 개는 무조건 남기고, 그보다 오래되고 days 를 넘긴 것만 지운다.
418
510
  */
419
511
  export function prune(root, { keep = 30, days = 30 } = {}) {
420
- // 지울지 말지는 파일 시각만 보면 안다. 내용은 필요 없다 —
421
- // 여기서 전부 읽던 것이 켤 때 몇 초씩 멈추던 원인이었다.
422
- const all = list(root, { limit: 1000, 속까지: false });
512
+ /*
513
+ * 지울지 말지는 파일 시각만 보면 안다. 내용은 필요 없다 —
514
+ * 여기서 전부 읽던 것이 켤 때 몇 초씩 멈추던 원인이었다.
515
+ *
516
+ * 개수를 1000 으로 끊어 놓으면 정작 **제일 오래된 것들**이 그 뒤에 있어서
517
+ * 영영 안 지워진다. 폴더가 끝없이 자라지 말라고 있는 함수가 딱 그 일을
518
+ * 못 하게 된다. 열지도 않고 시각만 보는 목록이라 전부 봐도 싸다.
519
+ */
520
+ const all = list(root, { limit: Infinity, 속까지: false });
423
521
  const 자를것 = all.slice(keep).filter((s) => (Date.now() - s.at.getTime()) > days * 86400000);
424
- for (const s of 자를것) { try { rmSync(s.file, { force: true }); } catch {} }
425
- return 자를것.length;
522
+ // 지웠다고 세는 것은 **정말 지워진 것**만이다.
523
+ let 지운수 = 0;
524
+ for (const s of 자를것) { try { rmSync(s.file, { force: true }); 지운수 += 1; } catch { /* 잡혀 있으면 다음에 */ } }
525
+ return 지운수;
426
526
  }
package/src/commands.js CHANGED
@@ -1672,11 +1672,18 @@ export async function handle(line, session, ctx) {
1672
1672
  return { handled: true };
1673
1673
  }
1674
1674
  for (const [i, r] of rows.entries()) {
1675
- say(` ${i === 0 ? c.hgreen('●') : c.gray('·')} ${c.bold(pad(r.id, 17))}${c.gray(pad(`${r.turns}턴`, 6, 'right'))} ${c.gray(clip(r.model, 22))}`);
1676
- say(` ${c.gray(clip(r.first, 66))}`);
1675
+ // 못 읽은 것도 올린다 (agent/store.js 의 list). 없는 것과 못 여는 것은 다르다.
1676
+ const 앞 = r.못읽음 ? c.yellow(mark.warn) : i === 0 ? c.hgreen('●') : c.gray('·');
1677
+ say(` ${앞} ${c.bold(pad(r.id, 17))}${c.gray(pad(r.못읽음 ? '' : `${r.turns}턴`, 6, 'right'))} ${c.gray(clip(r.model, 22))}`);
1678
+ say(` ${r.못읽음 ? c.yellow(clip(r.first, 66)) : c.gray(clip(r.first, 66))}`);
1677
1679
  }
1678
1680
  say('');
1679
- say(` ${c.gray('이어하려면 나갔다가')} ${c.cyan('deel --continue')} ${c.gray('또는')} ${c.cyan(`deel --resume ${rows[0].id}`)}`);
1681
+ const 읽히는것 = rows.filter((r) => !r.못읽음);
1682
+ if (읽히는것.length) {
1683
+ say(` ${c.gray('이어하려면 나갔다가')} ${c.cyan('deel --continue')} ${c.gray('또는')} ${c.cyan(`deel --resume ${읽히는것[0].id}`)}`);
1684
+ } else {
1685
+ say(` ${mark.warn} ${c.yellow('여기 있는 대화를 하나도 못 열었습니다 — 이어할 수 없습니다.')}`);
1686
+ }
1680
1687
  /*
1681
1688
  * 바로 아래 줄이 약속이다. 못 지키고 있으면 약속을 되풀이하면 안 된다.
1682
1689
  *
@@ -2584,6 +2591,18 @@ async function 커밋명령(session, ctx, arg = '') {
2584
2591
  say('');
2585
2592
  say(` ${c.yellow('모델이 메시지를 못 만들어 바뀐 것만 적었습니다.')}`);
2586
2593
  }
2594
+ /*
2595
+ * diff 를 못 읽고 지은 메시지라면 그렇다고 적는다.
2596
+ *
2597
+ * 담긴 것이 64MB 를 넘으면 git 이 몸통을 못 뱉는다. 파일 목록은 멀쩡해서
2598
+ * 커밋은 그대로 이어지는데, 그때 메시지는 **내용을 한 줄도 안 보고** 지은
2599
+ * 것이다. 그 사실이 화면에 없으면 사람은 여느 메시지와 똑같이 믿는다.
2600
+ */
2601
+ if (r.diff못읽음) {
2602
+ say('');
2603
+ say(` ${c.yellow('내용(diff)을 못 읽고 지은 메시지입니다')} ${c.gray(`— ${clip(r.diff못읽음, 60)}`)}`);
2604
+ say(` ${c.gray('파일 이름과 줄 수만 보고 썼습니다. 찍기 전에 한 번 읽어 주세요.')}`);
2605
+ }
2587
2606
 
2588
2607
  const 상태줄 = r.상태 ? r.상태.split('\n') : [];
2589
2608
  if (상태줄.length) {