-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
5716 lines (5153 loc) · 242 KB
/
Copy pathapp.js
File metadata and controls
5716 lines (5153 loc) · 242 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
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
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
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
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
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
const content = {
ko: {
runReady: "준비됨",
runDone: "실행 완료",
runSaved: "저장 완료",
runError: "실행 오류",
outputReady:
"실행 결과가 여기에 표시됩니다.\n\n힌트:\n- 대부분의 언어는 클라우드 런타임으로 실제 실행됩니다.\n- HTML은 미리보기 프레임에서 렌더링됩니다.\n- Arduino/ESP-IDF는 회로도 템플릿을 함께 제공합니다.",
ideaFallback: "예제를 찾는 중이라면, 간단한 계산기나 숫자 맞추기 게임부터 시작해 보세요.",
learningIntro: "선택한 언어 학습 포인트",
errorEmpty: "오류 메시지를 입력하면 원인과 해결 방향을 제안합니다.",
saveEmpty: "프로젝트 이름을 먼저 입력하세요.",
saveMetaEmpty: "아직 저장된 프로젝트가 없습니다.",
libraryEmpty: "저장된 프로젝트가 없습니다. 저장 버튼으로 현재 코드를 보관해 보세요.",
homeEmpty: "아직 저장된 프로젝트가 없습니다. 에디터 페이지에서 첫 프로젝트를 저장해 보세요.",
},
en: {
runReady: "Ready",
runDone: "Run complete",
runSaved: "Saved",
runError: "Run error",
outputReady:
"Execution results will appear here.\n\nHints:\n- Most languages run on a cloud runtime.\n- HTML renders in preview frame.\n- Arduino/ESP-IDF include a circuit template panel.",
ideaFallback: "If you need a starter idea, build a calculator or a number guessing game first.",
learningIntro: "Learning points for the selected language",
errorEmpty: "Paste an error message to get a likely cause and fix direction.",
saveEmpty: "Enter a project name first.",
saveMetaEmpty: "No saved project yet.",
libraryEmpty: "No saved projects yet. Use Save to keep the current draft.",
homeEmpty: "No saved projects yet. Save your first project from the editor page.",
},
ja: {
runReady: "準備完了",
runDone: "実行完了",
runSaved: "保存完了",
runError: "実行エラー",
outputReady: "Execution results will appear here.",
ideaFallback: "Try a small calculator or guessing game project first.",
learningIntro: "Learning points for the selected language",
errorEmpty: "Paste an error message to get a likely cause and fix direction.",
saveEmpty: "Enter a project name first.",
saveMetaEmpty: "No saved project yet.",
libraryEmpty: "No saved projects yet.",
homeEmpty: "No saved projects yet.",
},
es: {
runReady: "Listo",
runDone: "Ejecucion completa",
runSaved: "Guardado",
runError: "Error de ejecucion",
outputReady: "Execution results will appear here.",
ideaFallback: "Try a small calculator or guessing game project first.",
learningIntro: "Learning points for the selected language",
errorEmpty: "Paste an error message to get a likely cause and fix direction.",
saveEmpty: "Enter a project name first.",
saveMetaEmpty: "No saved project yet.",
libraryEmpty: "No saved projects yet.",
homeEmpty: "No saved projects yet.",
},
};
const storageKey = "vpr-projects";
const lastProjectKey = "vpr-last-project";
const localeKey = "vpr-locale";
const themeKey = "vpr-theme";
const pendingSnippetKey = "vpr-pending-snippet";
const usersKey = "vpr-users";
const sessionUserKey = "vpr-session-user";
const publicSnippetsKey = "vpr-public-snippets";
const runThrottleKey = "vpr-run-throttle-window";
const runtimeConfigKey = "vpr-runtime-config";
const circuitStateKey = "vpr-circuit-state";
const aiConfigKey = "vpr-ai-config";
const aiChatRoomsKey = "vpr-ai-chat-rooms";
const aiActiveChatRoomKey = "vpr-ai-active-chat-room";
const homeMemoKey = "vpr-home-memo";
const homeTodoKey = "vpr-home-todos";
const homeBrowserUrlKey = "vpr-home-browser-url";
const defaultRuntimeEndpoint = "http://localhost:3000/api/v2/piston/execute";
const defaultAiProxyEndpoint = "http://localhost:3000/api/ai/chat";
const defaultAiModelAuditEndpoint = "http://localhost:3000/api/ai/models/audit";
const defaultDataApiBase = "http://localhost:3000/api/data";
const localeOptions = {
ko: "한국어",
en: "English",
ja: "日本語",
es: "Español",
};
const aiProviderPresets = {
huggingface: {
provider: "huggingface",
endpoint: "https://router.huggingface.co/hf-inference/models",
model: "Qwen/Qwen2.5-Coder-32B-Instruct",
apiKey: "",
},
};
// Hugging Face hf-inference candidate models
const huggingFaceModels = [
{ name: "Qwen2.5 Coder 32B (code-generation)", value: "Qwen/Qwen2.5-Coder-32B-Instruct" },
{ name: "Carbon-3B (text-generation)", value: "HuggingFaceBio/Carbon-3B" },
{ name: "BART Large CNN (요약)", value: "facebook/bart-large-cnn" },
{ name: "T5 Small (번역/일반 텍스트)", value: "google-t5/t5-small" },
{ name: "T5 Base (번역/일반 텍스트)", value: "google-t5/t5-base" },
{ name: "PEGASUS XSum (요약)", value: "google/pegasus-xsum" },
{ name: "BART Large XSum (요약)", value: "facebook/bart-large-xsum" },
{ name: "MADLAD400 3B MT (다국어 번역)", value: "google/madlad400-3b-mt" },
{ name: "Falconsai Text Summarization", value: "Falconsai/text_summarization" },
];
const huggingFaceAuditCandidates = [
...huggingFaceModels.map((item) => item.value),
"google/bigbird-pegasus-large-arxiv",
"google/madlad400-3b-mt",
"facebook/bart-large-xsum",
"google/pegasus-xsum",
"google-t5/t5-large",
"google-t5/t5-small",
"google-t5/t5-base",
"HuggingFaceBio/Carbon-3B",
"Falconsai/text_summarization",
];
const languageSamples = {
javascript: {
label: "JavaScript",
extension: "js",
runner: { language: "javascript", version: "18.15.0" },
guide: [
"브라우저 DOM과 런타임 API를 구분하세요.",
"작은 함수 단위로 분리하면 디버깅이 쉬워집니다.",
"실행 전 입력값 검증을 습관화하세요.",
],
code: "console.log('Hello from JavaScript');\nconsole.log(2 + 3);",
},
typescript: {
label: "TypeScript",
extension: "ts",
runner: { language: "typescript", version: "5.0.3" },
guide: ["타입 추론을 활용하되 핵심 API는 명시 타입을 지정하세요.", "interface와 type의 용도를 구분하세요."],
code: "const sum = (a: number, b: number): number => a + b;\nconsole.log(sum(2, 5));",
},
python: {
label: "Python",
extension: "py",
runner: { language: "python", version: "3.10.0" },
guide: ["들여쓰기 정렬을 항상 일정하게 유지하세요.", "Traceback 마지막 줄부터 원인 파악을 시작하세요."],
code: "def greet(name):\n return f'Hello, {name}'\n\nprint(greet('VPR'))",
},
c: {
label: "C",
extension: "c",
runner: { language: "c", version: "10.2.0" },
guide: ["헤더, 타입, 포인터 초기화를 꼼꼼히 확인하세요.", "경계 검사 없는 배열 접근을 피하세요."],
code: "#include <stdio.h>\n\nint main(void) {\n printf(\"Hello from C\\n\");\n return 0;\n}",
},
cpp: {
label: "C++",
extension: "cpp",
runner: { language: "c++", version: "10.2.0" },
guide: ["컴파일 에러와 런타임 에러를 분리해 해석하세요.", "STL 사용 시 범위 체크를 우선하세요."],
code: "#include <iostream>\n\nint main() {\n std::cout << \"Hello from C++\\n\";\n return 0;\n}",
},
csharp: {
label: "C#",
extension: "cs",
runner: { language: "csharp", version: "6.12.0" },
guide: ["null 가능 참조를 고려한 방어 코드를 작성하세요.", "작은 메서드 단위로 책임을 분리하세요."],
code: "using System;\n\nclass Program {\n static void Main() {\n Console.WriteLine(\"Hello from C#\");\n }\n}",
},
java: {
label: "Java",
extension: "java",
runner: { language: "java", version: "15.0.2" },
guide: ["클래스명과 파일명 일치를 지키세요.", "예외 메시지를 그대로 숨기지 마세요."],
code: "public class Main {\n public static void main(String[] args) {\n System.out.println(\"Hello from Java\");\n }\n}",
},
go: {
label: "Go",
extension: "go",
runner: { language: "go", version: "1.16.2" },
guide: ["error 반환값을 무시하지 마세요.", "패키지 구조를 단순하게 유지하세요."],
code: "package main\n\nimport \"fmt\"\n\nfunc main() {\n fmt.Println(\"Hello from Go\")\n}",
},
rust: {
label: "Rust",
extension: "rs",
runner: { language: "rust", version: "1.89.0" },
guide: ["소유권과 빌림 규칙을 먼저 이해하세요.", "Result와 Option을 패턴 매칭으로 처리하세요."],
code: "fn main() {\n println!(\"Hello from Rust\");\n}",
},
fortran: {
label: "Fortran",
extension: "f90",
runner: { language: "fortran", version: "13.2.0" },
guide: ["program / end program 구조를 유지하세요.", "입출력 형식과 배열 인덱스를 주의하세요."],
code: "program main\n print *, 'Hello from Fortran'\nend program main",
},
zig: {
label: "Zig",
extension: "zig",
runner: { language: "zig", version: "0.16.0" },
guide: ["명시적 에러 처리와 타입을 유지하세요.", "표준 라이브러리 import를 최소 단위로 시작하세요."],
code: "const std = @import(\"std\");\n\npub fn main() void {\n std.debug.print(\"Hello from Zig\\n\", .{});\n}",
},
asm: {
label: "ASM (auto)",
extension: "s",
runner: { language: "asm", version: "gcc-as" },
guide: ["기본 ASM은 GCC/GAS x64 문법 기준으로 실행됩니다.", "세부 방언이 필요하면 ASM (x64 / GAS), ASM (NASM)을 선택하세요."],
code: ".intel_syntax noprefix\n.section .rdata,\"dr\"\nmsg: .asciz \"Hello from ASM\"\n\n.text\n.globl main\n.extern puts\nmain:\n sub rsp, 40\n lea rcx, msg[rip]\n call puts\n xor eax, eax\n add rsp, 40\n ret",
},
"asm-x64": {
label: "ASM (x64 / GAS)",
extension: "s",
runner: { language: "asm-x64", version: "gcc-as x64" },
guide: ["Windows x64 + GNU as 문법용입니다.", "`.intel_syntax noprefix`, `.globl main`, `rip` 상대 주소를 사용하세요."],
code: ".intel_syntax noprefix\n.section .rdata,\"dr\"\nmsg: .asciz \"Hello from ASM x64\"\n\n.text\n.globl main\n.extern puts\nmain:\n sub rsp, 40\n lea rcx, msg[rip]\n call puts\n xor eax, eax\n add rsp, 40\n ret",
},
"asm-nasm": {
label: "ASM (NASM)",
extension: "asm",
runner: { language: "asm-nasm", version: "nasm" },
guide: ["NASM 문법(`section .text`, `global main`)용입니다.", "[rel 레이블] RIP-relative 주소 지정을 반드시 사용하세요."],
code: "section .data\n msg db \"Hello from NASM\", 0\n\nsection .text\n global main\n extern puts\n main:\n sub rsp, 40\n lea rcx, [rel msg]\n call puts\n xor eax, eax\n add rsp, 40\n ret",
},
ruby: {
label: "Ruby",
extension: "rb",
runner: { language: "ruby", version: "3.0.1" },
guide: ["메서드 책임을 작게 유지하세요.", "동적 타입이라도 입력 검증은 필수입니다."],
code: "puts 'Hello from Ruby'",
},
sql: {
label: "SQL",
extension: "sql",
runner: { language: "sql", version: "3.40.0" },
guide: ["SELECT 전에 테이블 구조를 먼저 설계하세요.", "WHERE 조건 없는 UPDATE/DELETE는 위험합니다."],
code: "SELECT 'Hello from SQL' AS greeting;\nSELECT 3 + 7 AS result;",
},
powershell: {
label: "PowerShell",
extension: "ps1",
runner: { language: "powershell", version: "7.3.0" },
guide: ["파이프라인(|)으로 커맨드를 연결하세요.", "오류 처리에 try/catch를 사용하세요."],
code: "Write-Output 'Hello from PowerShell'\nWrite-Output (3 + 7)",
},
bash: {
label: "Bash",
extension: "sh",
runner: { language: "bash", version: "5.2.0" },
guide: ["변수에 공백 없이 대입하세요 (a=1).", "문자열 비교는 == 대신 = 또는 [[ ]] 사용을 권장합니다."],
code: "#!/bin/bash\necho \"Hello from Bash\"\necho $((3 + 7))",
},
html: {
label: "HTML",
extension: "html",
mode: "preview",
guide: ["시맨틱 태그를 우선 사용하세요.", "스타일/동작 분리를 유지하세요."],
code: "<!DOCTYPE html>\n<html lang=\"ko\">\n <body>\n <h1>Hello from HTML</h1>\n <p>Preview works in the panel.</p>\n </body>\n</html>",
},
css: {
label: "CSS",
extension: "css",
mode: "preview",
guide: ["컴포넌트 단위 클래스로 스타일 범위를 관리하세요.", "간격/색상은 변수로 관리하면 유지보수가 쉬워집니다."],
code: ":root {\n --bg: #f2efe9;\n --ink: #1f2a37;\n --accent: #d97706;\n}\n\nbody {\n margin: 0;\n min-height: 100vh;\n display: grid;\n place-items: center;\n font-family: 'Segoe UI', sans-serif;\n background: radial-gradient(circle at top, #fff, var(--bg));\n}\n\n.card {\n width: min(420px, 90vw);\n padding: 24px;\n border-radius: 20px;\n background: #fff;\n box-shadow: 0 12px 30px rgba(31, 42, 55, 0.16);\n}\n\n.title {\n margin: 0 0 8px;\n color: var(--ink);\n}\n\n.badge {\n display: inline-block;\n margin-top: 12px;\n padding: 6px 10px;\n border-radius: 999px;\n background: var(--accent);\n color: #fff;\n font-size: 12px;\n}\n",
},
arduino: {
label: "Arduino",
extension: "ino",
mode: "circuit",
guide: ["핀 번호와 전원 연결을 먼저 설계하세요.", "센서 전압 범위 확인 후 연결하세요."],
code: "void setup() {\n Serial.begin(9600);\n pinMode(13, OUTPUT);\n}\n\nvoid loop() {\n digitalWrite(13, HIGH);\n delay(500);\n digitalWrite(13, LOW);\n delay(500);\n}",
},
espidf: {
label: "ESP-IDF",
extension: "c",
mode: "circuit",
guide: ["ESP32 핀맵과 전원 요구사항을 먼저 확인하세요.", "FreeRTOS task 분리를 작게 시작하세요."],
code: "#include <stdio.h>\n#include \"freertos/FreeRTOS.h\"\n#include \"freertos/task.h\"\n\nvoid app_main(void) {\n while (1) {\n printf(\"Hello from ESP-IDF\\n\");\n vTaskDelay(1000 / portTICK_PERIOD_MS);\n }\n}",
},
};
const ideaTemplates = [
{
match: /게임|game/i,
text: "초보자용 게임 아이디어\n1. 숫자 맞추기 게임\n2. 반응 속도 측정 게임\n3. 사칙연산 퀴즈 게임",
},
{
match: /임베디드|arduino|esp/i,
text: "임베디드 아이디어\n1. 온습도 모니터\n2. LED 패턴 제어기\n3. Wi-Fi 상태 알림 장치",
},
{
match: /웹|html|js|javascript/i,
text: "웹 프로젝트 아이디어\n1. 메모장형 온라인 에디터\n2. 학습 타이머\n3. 에러 메시지 해설 페이지",
},
];
const errorAnalysisRules = [
{
id: "syntax",
title: "문법 오류",
match: /syntaxerror|invalid syntax|unexpected token|expected .* before|missing .*;|cs1002|unterminated string|indentationerror|taberror/i,
cause: "괄호/따옴표/세미콜론/콜론 누락 또는 블록 정렬 문제 가능성이 큽니다.",
fixes: [
"에러가 표시된 줄과 바로 윗줄의 괄호(), 중괄호{}, 대괄호[] 짝을 먼저 맞추세요.",
"문자열 시작/종료 따옴표가 같은지 확인하세요.",
"C/C++/Java/C# 계열은 줄 끝 세미콜론(;) 누락을 확인하세요.",
"Python은 들여쓰기(공백 수)와 콜론(:) 누락을 확인하세요.",
],
},
{
id: "name",
title: "이름/심볼 미정의",
match: /nameerror|is not defined|cannot find symbol|undeclared identifier|cs0103|referenceerror/i,
cause: "변수/함수/클래스 이름 오타 또는 선언 전에 사용한 경우가 많습니다.",
fixes: [
"에러 이름의 철자와 대소문자를 선언부와 정확히 일치시키세요.",
"해당 심볼이 사용되기 전에 선언/초기화되는지 확인하세요.",
"파일 분리 프로젝트라면 import/include using 문을 확인하세요.",
],
},
{
id: "null",
title: "널/없음 값 접근",
match: /nullreferenceexception|cannot read propert|cannot read .* of undefined|none ?type|attributeerror: 'none|undefined|nil pointer|object reference not set/i,
cause: "값이 없는 상태(null/undefined/None)에서 속성이나 메서드 접근이 발생했습니다.",
fixes: [
"접근 전에 null/undefined/None 체크를 추가하세요.",
"함수 반환값이 비어 있을 수 있는 경로를 처리하세요.",
"초기화 순서를 점검하고 기본값을 지정하세요.",
],
},
{
id: "index",
title: "인덱스/범위 오류",
match: /indexerror|out of range|arrayindexoutofboundsexception|rangeerror|subscript out of range/i,
cause: "배열/리스트 길이를 넘는 인덱스를 접근한 상황입니다.",
fixes: [
"반복문 종료 조건을 < length 형태로 점검하세요.",
"빈 배열일 수 있는 경우를 먼저 처리하세요.",
"사용 전 인덱스 유효성 검사(0 <= i < length)를 추가하세요.",
],
},
{
id: "module",
title: "모듈/패키지 로드 실패",
match: /modulenotfounderror|cannot find module|no module named|importerror|cannot resolve module|package .* does not exist/i,
cause: "의존성 설치 누락, 경로 오타, 런타임 환경 차이 가능성이 큽니다.",
fixes: [
"모듈 이름 오타와 대소문자를 확인하세요.",
"실행 환경에 해당 패키지가 설치되어 있는지 확인하세요.",
"상대경로 import는 현재 파일 기준 경로를 다시 확인하세요.",
],
},
{
id: "type",
title: "타입 불일치",
match: /typeerror|cannot convert|invalid literal|classcastexception|cannot assign|operator .* not supported|argument of type/i,
cause: "함수 인자, 연산 대상, 반환 타입이 기대 타입과 다릅니다.",
fixes: [
"문자열/숫자 변환 위치를 명시적으로 처리하세요.",
"함수 시그니처(인자 개수/타입)와 호출부를 맞추세요.",
"동적 언어도 입력값 타입 검증 코드를 추가하세요.",
],
},
{
id: "memory",
title: "메모리/포인터 오류",
match: /segmentation fault|access violation|stack overflow|double free|invalid pointer|core dumped/i,
cause: "잘못된 포인터 접근, 해제 후 접근, 과도한 재귀/스택 사용 가능성이 있습니다.",
fixes: [
"포인터/참조가 유효한 시점에만 접근하세요.",
"해제(free/delete) 이후 재사용 여부를 확인하세요.",
"재귀 깊이를 줄이거나 반복문으로 대체하세요.",
],
},
{
id: "timeout",
title: "시간 초과/무한 루프",
match: /time.?out|timed out|execution time limit|infinite loop|maximum call stack size exceeded/i,
cause: "종료 조건 누락 또는 과도한 연산으로 실행 제한 시간을 초과했습니다.",
fixes: [
"반복문/재귀 종료 조건이 반드시 참이 되는지 확인하세요.",
"입력 크기에 따른 시간복잡도를 줄이세요.",
"디버그 출력으로 루프 변수 변화를 추적하세요.",
],
},
{
id: "io",
title: "입력/출력 처리 오류",
match: /eof when reading a line|input mismatch|scanf|scanner|valueerror: invalid literal for int|numberformatexception/i,
cause: "입력 형식이 코드가 기대하는 형식과 다르거나 입력이 부족합니다.",
fixes: [
"입력 개수/형식을 문제 요구사항과 동일하게 맞추세요.",
"숫자 파싱 전에 trim, 빈 문자열 체크를 추가하세요.",
"공백/개행 단위 입력 처리 로직을 점검하세요.",
],
},
];
const snippets = [
{
title: "숫자 맞추기 게임",
description: "조건문과 반복문을 연습하는 입문용 콘솔 프로젝트",
language: "javascript",
code: "const answer = 7;\nconst guess = 5;\nif (guess === answer) {\n console.log('정답입니다!');\n} else {\n console.log('다시 시도하세요.');\n}",
},
{
title: "HTML 포트폴리오 페이지",
description: "태그 구조와 CSS 레이아웃을 배우기 좋은 예제",
language: "html",
code: "<!DOCTYPE html>\n<html lang=\"ko\">\n <body>\n <main><h1>홍길동 포트폴리오</h1><p>웹 프론트엔드 입문 학습 중입니다.</p></main>\n </body>\n</html>",
},
{
title: "Arduino Blink",
description: "LED 제어와 핀 설정 입문 예제",
language: "arduino",
code: languageSamples.arduino.code,
},
];
let currentLocale = localStorage.getItem(localeKey) || "ko";
if (!Object.prototype.hasOwnProperty.call(content, currentLocale)) {
currentLocale = "en";
}
function getCurrentTheme() {
const raw = localStorage.getItem(themeKey);
return raw === "dark" ? "dark" : "light";
}
function applyTheme(theme) {
const next = theme === "dark" ? "dark" : "light";
localStorage.setItem(themeKey, next);
document.body.setAttribute("data-theme", next);
}
function applyThemeSelections() {
document.querySelectorAll("#themeSelect").forEach((select) => {
select.value = getCurrentTheme();
select.addEventListener("change", (event) => {
applyTheme(event.target.value);
});
});
}
function populateLocaleSelect(select) {
if (!select) {
return;
}
select.innerHTML = Object.entries(localeOptions)
.map(([value, label]) => `<option value="${value}">${label}</option>`)
.join("");
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function sanitizeName(value) {
return String(value).trim().replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 20);
}
async function dataApiGet(pathname) {
const response = await fetch(`${defaultDataApiBase}${pathname}`, { method: "GET" });
if (!response.ok) {
throw new Error(`Data API GET failed (${response.status})`);
}
return response.json();
}
async function dataApiPost(pathname, payload) {
const response = await fetch(`${defaultDataApiBase}${pathname}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Data API POST failed (${response.status})`);
}
return response.json();
}
function syncProjectsToSql(projects) {
dataApiPost("/projects/bulk", { projects }).catch(() => {});
}
function syncUsersToSql(users) {
dataApiPost("/users/bulk", { users }).catch(() => {});
}
function syncPublicSnippetsToSql(items) {
dataApiPost("/public-snippets/bulk", { items }).catch(() => {});
}
async function hydrateLocalStorageFromSql() {
try {
const health = await dataApiGet("/health");
if (!health?.ok) {
return;
}
const [projectsRes, usersRes, snippetsRes] = await Promise.all([
dataApiGet("/projects"),
dataApiGet("/users"),
dataApiGet("/public-snippets"),
]);
if (Array.isArray(projectsRes.projects)) {
localStorage.setItem(storageKey, JSON.stringify(projectsRes.projects));
}
if (Array.isArray(usersRes.users)) {
localStorage.setItem(usersKey, JSON.stringify(usersRes.users));
}
if (Array.isArray(snippetsRes.items)) {
localStorage.setItem(publicSnippetsKey, JSON.stringify(snippetsRes.items));
}
} catch {
// SQL store unavailable: keep localStorage-only behavior
}
}
function getProjects() {
try {
return JSON.parse(localStorage.getItem(storageKey) || "[]");
} catch {
return [];
}
}
function setProjects(projects) {
localStorage.setItem(storageKey, JSON.stringify(projects));
syncProjectsToSql(projects);
}
function getUsers() {
try {
return JSON.parse(localStorage.getItem(usersKey) || "[]");
} catch {
return [];
}
}
function setUsers(users) {
localStorage.setItem(usersKey, JSON.stringify(users));
syncUsersToSql(users);
}
function getCurrentUser() {
return localStorage.getItem(sessionUserKey);
}
function setCurrentUser(username) {
if (username) {
localStorage.setItem(sessionUserKey, username);
} else {
localStorage.removeItem(sessionUserKey);
}
}
function getPublicSnippets() {
try {
return JSON.parse(localStorage.getItem(publicSnippetsKey) || "[]");
} catch {
return [];
}
}
function setPublicSnippets(items) {
localStorage.setItem(publicSnippetsKey, JSON.stringify(items));
syncPublicSnippetsToSql(items);
}
function formatTimestamp(timestamp) {
return new Intl.DateTimeFormat(currentLocale === "ko" ? "ko-KR" : "en-US", {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(timestamp));
}
function buildIdeaResponse(prompt) {
const found = ideaTemplates.find((template) => template.match.test(prompt));
return found ? found.text : content[currentLocale].ideaFallback;
}
function guessLanguageFromErrorLog(message) {
const rules = [
{ lang: "python", match: /traceback|syntaxerror|indentationerror|modulenotfounderror|nameerror|attributeerror/i },
{ lang: "javascript", match: /referenceerror|cannot read properties|node:internal|npm|at .*\(.+:\d+:\d+\)/i },
{ lang: "typescript", match: /ts\d{4}|typescript/i },
{ lang: "java", match: /exception in thread|\.java:\d+|cannot find symbol|javac/i },
{ lang: "csharp", match: /cs\d{4}|unhandled exception|system\./i },
{ lang: "cpp", match: /\.cpp:\d+|g\+\+|std::|undefined reference|segmentation fault/i },
{ lang: "c", match: /\.c:\d+|clang|gcc|scanf|printf/i },
{ lang: "go", match: /panic:|goroutine|\.go:\d+|go build/i },
{ lang: "ruby", match: /nomethoderror|undefined method|\.rb:\d+/i },
{ lang: "sql", match: /sql syntax|sqlite|postgres|mysql|near ".*": syntax error/i },
{ lang: "bash", match: /command not found|permission denied|line \d+:|bash:/i },
{ lang: "powershell", match: /at line:\d+ char:\d+|fullyqualifiederrorid|powershell/i },
];
const found = rules.find((rule) => rule.match.test(message));
return found ? found.lang : "unknown";
}
function extractErrorLocation(message) {
const patterns = [
/File\s+"([^"]+)",\s+line\s+(\d+)/i,
/at\s+.*\(([^)\n]+):(\d+):(\d+)\)/i,
/([\w./\\-]+\.(?:js|ts|py|java|cs|go|c|cpp|rb|sql|sh|ps1)):(\d+):(\d+)/i,
/([\w./\\-]+\.(?:js|ts|py|java|cs|go|c|cpp|rb|sql|sh|ps1)):(\d+)/i,
/([\w./\\-]+\.(?:cs))\((\d+),(\d+)\)/i,
/line\s+(\d+)/i,
];
for (const pattern of patterns) {
const match = message.match(pattern);
if (!match) continue;
if (pattern.source.includes("File\\s+\"")) {
return { file: match[1], line: Number(match[2]), column: null, confidence: "high" };
}
if (pattern.source.includes("at\\s+.*\\(")) {
return { file: match[1], line: Number(match[2]), column: Number(match[3]), confidence: "high" };
}
if (pattern.source.includes("\\.(?:js|ts|py|java|cs|go|c|cpp|rb|sql|sh|ps1)):(\\d+):(\\d+)")) {
return { file: match[1], line: Number(match[2]), column: Number(match[3]), confidence: "high" };
}
if (pattern.source.includes("\\.(?:cs))\\((\\d+),(\\d+)\\)")) {
return { file: match[1], line: Number(match[2]), column: Number(match[3]), confidence: "high" };
}
if (pattern.source.includes("\\.(?:js|ts|py|java|cs|go|c|cpp|rb|sql|sh|ps1)):(\\d+)")) {
return { file: match[1], line: Number(match[2]), column: null, confidence: "medium" };
}
if (pattern.source.includes("line\\s+(\\d+)")) {
return { file: null, line: Number(match[1]), column: null, confidence: "low" };
}
}
return { file: null, line: null, column: null, confidence: "low" };
}
function findMatchedErrorRules(message) {
const matched = errorAnalysisRules.filter((rule) => rule.match.test(message));
return matched.length > 0 ? matched : [];
}
function estimateCodeAtLine(relatedCode, lineNumber) {
if (!relatedCode || !lineNumber) {
return null;
}
const lines = String(relatedCode).split(/\r?\n/);
const index = lineNumber - 1;
if (index < 0 || index >= lines.length) {
return null;
}
const target = lines[index]?.trim();
if (!target) {
return null;
}
return target.length > 200 ? `${target.slice(0, 200)}...` : target;
}
function buildGeneralFallbackTips(language) {
const languageSpecific = {
python: "Python은 Traceback의 마지막 예외 줄 바로 위의 File/line 위치부터 확인하세요.",
javascript: "JavaScript는 스택트레이스의 가장 위 사용자 코드 파일:줄부터 확인하세요.",
typescript: "TypeScript는 컴파일 오류 코드(TSxxxx)와 해당 줄 타입 선언을 같이 확인하세요.",
java: "Java는 첫 번째 컴파일 오류를 먼저 해결하면 연쇄 오류가 함께 사라지는 경우가 많습니다.",
csharp: "C#은 CS 오류 코드별 원인(예: CS1002 ; 누락)을 먼저 확인하세요.",
c: "C는 경고를 무시하지 말고 포인터/배열 접근 경계를 먼저 점검하세요.",
cpp: "C++는 템플릿/타입 오류가 길게 나오므로 첫 번째 error 줄부터 순서대로 해결하세요.",
go: "Go는 컴파일러 메시지의 파일:줄:열 포맷을 그대로 따라가면 빠릅니다.",
};
return [
languageSpecific[language] || "오류 로그의 첫 번째 파일/줄 정보부터 확인하세요.",
"최근 수정한 코드 블록만 임시로 최소화해 재실행하면 원인 분리가 쉬워집니다.",
"입력값이 필요한 프로그램이면 표준 입력 형식과 개수를 문제 요구와 동일하게 맞추세요.",
];
}
function buildErrorResponse(message, options = {}) {
const raw = String(message || "").trim();
if (!raw) {
return content[currentLocale].errorEmpty;
}
const providedLanguage = options.language || "";
const providedFile = String(options.fileName || "").trim();
const relatedCode = String(options.relatedCode || "");
const inferredLanguage = providedLanguage && providedLanguage !== "auto" ? providedLanguage : guessLanguageFromErrorLog(raw);
const location = extractErrorLocation(raw);
const detectedFile = location.file || providedFile || "(로그에서 파일명을 찾지 못함)";
const detectedLine = location.line || "(줄 번호 미확인)";
const detectedColumn = location.column || "(열 정보 없음)";
const matchedRules = findMatchedErrorRules(raw);
const estimatedCode = estimateCodeAtLine(relatedCode, Number(location.line));
const summaryLines = [
"[진단 요약]",
`- 추정 언어: ${inferredLanguage}`,
`- 추정 파일: ${detectedFile}`,
`- 추정 위치: line ${detectedLine}, col ${detectedColumn}`,
`- 위치 신뢰도: ${location.confidence}`,
];
const rootCauseLines = ["", "[가능한 원인]"];
if (matchedRules.length) {
matchedRules.slice(0, 3).forEach((rule, index) => {
rootCauseLines.push(`${index + 1}. ${rule.title}: ${rule.cause}`);
});
} else {
rootCauseLines.push("1. 로그 패턴이 일반 분류에 정확히 매칭되지 않았습니다.");
rootCauseLines.push("2. 가장 먼저 표시된 파일/줄의 직전 3~5줄 문맥에서 원인을 찾는 것이 효과적입니다.");
}
const fixLines = ["", "[권장 수정 순서]"];
if (matchedRules.length) {
const uniqueFixes = [];
matchedRules.forEach((rule) => {
rule.fixes.forEach((fix) => {
if (!uniqueFixes.includes(fix)) {
uniqueFixes.push(fix);
}
});
});
uniqueFixes.slice(0, 6).forEach((fix, index) => {
fixLines.push(`${index + 1}. ${fix}`);
});
} else {
buildGeneralFallbackTips(inferredLanguage).forEach((tip, index) => {
fixLines.push(`${index + 1}. ${tip}`);
});
}
const expectedCodeLines = ["", "[해당 위치에서 의심되는 코드 형태]"];
if (estimatedCode) {
expectedCodeLines.push(`- 제공한 코드 기준 line ${location.line}: ${estimatedCode}`);
} else if (location.line) {
expectedCodeLines.push(`- line ${location.line} 부근에서 선언 누락/타입 불일치/괄호 불균형 코드가 있을 가능성이 큽니다.`);
} else {
expectedCodeLines.push("- 파일/줄 정보가 부족합니다. 전체 로그(스택트레이스 포함)를 그대로 입력하면 정확도가 올라갑니다.");
}
const quickChecklist = [
"",
"[빠른 체크리스트]",
"1. 첫 번째 error/exception 메시지만 남기고 재실행해 2차 오류를 제거",
"2. 해당 줄 바로 위/아래 3줄 포함해 문법과 변수 선언 순서 확인",
"3. import/include/패키지 설치 여부 확인",
"4. 입력 형식(공백/개행/타입) 재검증",
];
return [...summaryLines, ...rootCauseLines, ...fixLines, ...expectedCodeLines, ...quickChecklist].join("\n");
}
function applyLocaleSelections() {
document.querySelectorAll("#localeSelect").forEach((select) => {
populateLocaleSelect(select);
select.value = currentLocale;
select.addEventListener("change", (event) => {
currentLocale = event.target.value;
localStorage.setItem(localeKey, currentLocale);
location.reload();
});
});
}
function getSecondaryHashFromUser(user) {
if (user?.profile?.secondPasswordHash) {
return String(user.profile.secondPasswordHash);
}
return String(user?.passwordHash || "");
}
async function registerUserAccount(usernameRaw, passwordRaw, secondPasswordRaw) {
const username = sanitizeName(usernameRaw);
const password = String(passwordRaw || "");
const secondPassword = String(secondPasswordRaw || "");
if (username.length < 3) {
throw new Error("아이디 형식이 올바르지 않습니다.");
}
if (password.length < 8) {
throw new Error("1차 비밀번호는 8자 이상이어야 합니다.");
}
if (secondPassword.length < 8) {
throw new Error("2차 비밀번호는 8자 이상이어야 합니다.");
}
const users = getUsers();
if (users.some((user) => user.username === username)) {
throw new Error("이미 존재하는 아이디입니다.");
}
const passwordHash = await hashPassword(password);
const secondPasswordHash = await hashPassword(secondPassword);
users.push({
username,
passwordHash,
profile: {
displayName: username,
bio: "",
favoriteLang: "",
secondPasswordHash,
imageDataUrl: "",
},
});
setUsers(users);
return username;
}
async function loginUserAccount(usernameRaw, passwordRaw, secondPasswordRaw) {
const username = sanitizeName(usernameRaw);
const users = getUsers();
const user = users.find((item) => item.username === username);
if (!user) {
throw new Error("로그인 실패: 아이디 또는 비밀번호를 확인하세요.");
}
const primaryHash = await hashPassword(String(passwordRaw || ""));
const secondaryHash = await hashPassword(String(secondPasswordRaw || ""));
if (user.passwordHash !== primaryHash || getSecondaryHashFromUser(user) !== secondaryHash) {
throw new Error("로그인 실패: 1차 또는 2차 비밀번호가 올바르지 않습니다.");
}
setCurrentUser(username);
return user;
}
async function verifyCurrentUserDualPassword(passwordRaw, secondPasswordRaw) {
const username = getCurrentUser();
if (!username) {
throw new Error("로그인이 필요합니다.");
}
const users = getUsers();
const user = users.find((item) => item.username === username);
if (!user) {
throw new Error("계정 정보를 찾을 수 없습니다.");
}
const primaryHash = await hashPassword(String(passwordRaw || ""));
const secondaryHash = await hashPassword(String(secondPasswordRaw || ""));
if (user.passwordHash !== primaryHash || getSecondaryHashFromUser(user) !== secondaryHash) {
throw new Error("비밀번호 확인에 실패했습니다.");
}
return { users, user };
}
function deleteAccountCompletely(username) {
const users = getUsers().filter((item) => item.username !== username);
setUsers(users);
const snippets = getPublicSnippets().filter((item) => item.author !== username);
setPublicSnippets(snippets);
if (getCurrentUser() === username) {
setCurrentUser(null);
}
}
function buildBasicExample(languageKey) {
const label = languageSamples[languageKey]?.label || languageKey;
switch (languageKey) {
case "javascript":
return `console.log('Hello from ${label}');\nconsole.log(1 + 1);`;
case "typescript":
return `console.log('Hello from ${label}');\nconsole.log(1 + 1);`;
case "python":
return `print('Hello from ${label}')\nprint(1 + 1)`;
case "c":
return `#include <stdio.h>\n\nint main(void) {\n printf(\"Hello from ${label}\\n\");\n printf(\"%d\\n\", 1 + 1);\n return 0;\n}`;
case "cpp":
return `#include <iostream>\n\nint main() {\n std::cout << \"Hello from ${label}\\n\";\n std::cout << (1 + 1) << \"\\n\";\n return 0;\n}`;
case "csharp":
return `using System;\n\nclass Program {\n static void Main() {\n Console.WriteLine(\"Hello from ${label}\");\n Console.WriteLine(1 + 1);\n }\n}`;
case "java":
return `public class Main {\n public static void main(String[] args) {\n System.out.println(\"Hello from ${label}\");\n System.out.println(1 + 1);\n }\n}`;
case "go":
return `package main\n\nimport \"fmt\"\n\nfunc main() {\n fmt.Println(\"Hello from ${label}\")\n fmt.Println(1 + 1)\n}`;
case "rust":
return `fn main() {\n println!(\"Hello from ${label}\");\n println!(\"{}\", 1 + 1);\n}`;
case "fortran":
return `program main\n print *, \"Hello from ${label}\"\n print *, 1 + 1\nend program main`;
case "zig":
return `const std = @import(\"std\");\n\npub fn main() void {\n std.debug.print(\"Hello from ${label}\\n\", .{});\n std.debug.print(\"{}\\n\", .{1 + 1});\n}`;
case "asm":
return `.intel_syntax noprefix\n.section .rdata,\"dr\"\nmsg: .asciz \"Hello from ${label}\"\n\n.text\n.globl main\n.extern puts\nmain:\n sub rsp, 40\n lea rcx, msg[rip]\n call puts\n xor eax, eax\n add rsp, 40\n ret`;
case "asm-x64":
return `.intel_syntax noprefix\n.section .rdata,\"dr\"\nmsg: .asciz \"Hello from ${label}\"\n\n.text\n.globl main\n.extern puts\nmain:\n sub rsp, 40\n lea rcx, msg[rip]\n call puts\n xor eax, eax\n add rsp, 40\n ret`;
case "asm-nasm":
return `section .data\nmsg db \"Hello from ${label}\", 0\n\nsection .text\nglobal main\nextern puts\nmain:\n sub rsp, 40\n lea rcx, [rel msg]\n call puts\n xor eax, eax\n add rsp, 40\n ret`;
case "ruby":
return `puts 'Hello from ${label}'\nputs 1 + 1`;
case "sql":
return `SELECT 'Hello from ${label}' AS message;\nSELECT 1 + 1 AS result;`;
case "powershell":
return `Write-Output 'Hello from ${label}'\nWrite-Output (1 + 1)`;
case "bash":
return `echo \"Hello from ${label}\"\necho $((1 + 1))`;
case "html":
return `<!DOCTYPE html>\n<html lang=\"ko\">\n <body>\n <h1>Hello from ${label}</h1>\n <p>1 + 1 = 2</p>\n </body>\n</html>`;
case "css":
return `body::before {\n content: \"Hello from ${label} | 1 + 1 = 2\";\n display: block;\n padding: 18px;\n font: 700 18px/1.4 'IBM Plex Sans KR', sans-serif;\n}`;
case "arduino":
return `void setup() {\n Serial.begin(9600);\n Serial.println(\"Hello from ${label}\");\n Serial.println(1 + 1);\n}\n\nvoid loop() {}`;
case "espidf":
return `#include <stdio.h>\n\nvoid app_main(void) {\n printf(\"Hello from ${label}\\n\");\n printf(\"%d\\n\", 1 + 1);\n}`;
default:
return `// Hello from ${label}\n// 1 + 1 = 2`;
}
}
function renderAuthShortcut() {
const sessionUser = getCurrentUser();
document.querySelectorAll(".topbar-actions").forEach((container) => {
const wrapper = document.createElement("div");
wrapper.className = "auth-inline";
if (sessionUser) {
wrapper.innerHTML = `
<span class="auth-chip">@${escapeHtml(sessionUser)}</span>
<a class="ghost-button" href="profile.html">프로필</a>`;
} else {
wrapper.innerHTML = `<a class="ghost-button" href="login.html">로그인</a>`;
}
container.appendChild(wrapper);
});
}
function populateLanguageSelect(select) {
if (!select) {
return;
}
select.innerHTML = Object.entries(languageSamples)
.map(([value, sample]) => `<option value="${value}">${sample.label}</option>`)
.join("");
}
function renderLearningGuide(target, languageKey) {
if (!target) {
return;
}
const sample = languageSamples[languageKey];
target.innerHTML = `${content[currentLocale].learningIntro}<br /><br />${sample.guide
.map((line) => `- ${line}`)
.join("<br />")}`;
}
function resetPreview(previewFrame) {
if (!previewFrame) {
return;
}
previewFrame.srcdoc = `
<body style="font-family: sans-serif; padding: 24px; color: #475569; background: #fff;">
<h3 style="margin-top: 0;">Preview standby</h3>
<p>HTML 코드를 실행하면 이 영역에 실제 결과가 표시됩니다.</p>
</body>`;
}
const componentTypes = {
arduinoBoards: {
"arduino-uno": "Arduino UNO",
"arduino-nano": "Arduino Nano",
"arduino-mega": "Arduino Mega 2560",
"arduino-leonardo": "Arduino Leonardo",
"arduino-due": "Arduino Due",
},
espBoards: {
"esp8266-01": "ESP8266-01",
"esp8266-12": "ESP8266-12E",
"esp32": "ESP32 DevKit",
"esp32-s2": "ESP32-S2",
"esp32-s3": "ESP32-S3",
"esp32-c2": "ESP32-C2",
"esp32-c3": "ESP32-C3",
"esp32-c6": "ESP32-C6",