diff --git a/README.md b/README.md
index b507125..c8c3893 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,12 @@ CodingTestKit was built to **replicate the real exam environment inside your IDE
| Python | O | O | O | O |
| C++ | O | O | O | O |
| Kotlin | O | X | O | O |
+| JavaScript | O | X | O | O |
+| Rust | X | X | O | O |
+| Go | O | X | O | O |
+| Ruby | O | X | O | O |
+
+> X = submission not supported on that platform. Local test execution works for all languages (compiler/runtime must be installed: JDK, python3, g++, kotlinc, Node.js, rustc, go, ruby). Selecting an unsupported combination shows a warning next to the platform/language selectors immediately.
---
@@ -83,12 +89,13 @@ CodingTestKit was built to **replicate the real exam environment inside your IDE
| 4 | [**Login & Submit**](docs/features/submit.md) | Submit code directly via built-in browser with auto language selection |
| 5 | [**Problem Search**](docs/features/search.md) | Search problems on LeetCode, Codeforces, Programmers, and SWEA |
| 6 | [**Random Problem Picker**](docs/features/random.md) | Pick random problems with tier/difficulty/tag filters |
-| 7 | [**Code Editor**](#code-editor) | Auto-generated boilerplate code per platform & language |
-| 8 | [**Code Templates**](docs/features/templates.md) | Save & reuse frequently used code snippets |
-| 9 | [**Timer**](docs/features/timer.md) | Stopwatch with laps + countdown with circular dial, progress bar, digital clock |
-| 10 | [**Settings & Exam Mode**](docs/features/exam-mode.md) | One-click exam mode: block paste, disable autocomplete, focus alert |
-| 11 | [**GitHub Integration**](docs/features/github.md) | Auto-push accepted solutions to GitHub |
-| 12 | [**Internationalization**](#internationalization-i18n) | Full Korean / English UI support |
+| 7 | **My Solved** | List your solved problems on LeetCode & Codeforces |
+| 8 | [**Code Editor**](#code-editor) | Auto-generated boilerplate code per platform & language |
+| 9 | [**Code Templates**](docs/features/templates.md) | Save & reuse frequently used code snippets |
+| 10 | [**Timer**](docs/features/timer.md) | Stopwatch with laps + countdown with circular dial, progress bar, digital clock |
+| 11 | [**Settings & Exam Mode**](docs/features/exam-mode.md) | One-click exam mode: block paste, disable autocomplete, focus alert |
+| 12 | [**GitHub Integration**](docs/features/github.md) | Auto-push accepted solutions to GitHub |
+| 13 | [**Internationalization**](#internationalization-i18n) | Full Korean / English UI support |
---
@@ -101,6 +108,8 @@ Select the platform and language, enter a problem number, and the problem descri
- **LeetCode**: Enter number, slug, or URL (e.g., `1`, `two-sum`, full URL)
- **Codeforces**: contestId+letter (e.g., `1234A`) or URL
+> Codeforces: if Cloudflare blocks the request, fetching automatically falls back to the built-in JCEF browser (may take ~5–20 seconds longer).
+
@@ -122,7 +131,7 @@ When a problem is fetched, a folder is automatically created with a code file an
### Problem View & Translation
-
+
View the problem description, I/O format, and examples directly in the plugin panel.
@@ -193,19 +202,11 @@ Programmers and LeetCode solution functions are automatically wrapped for testin
Log in to each platform via the built-in JCEF browser and submit your code directly. The language dropdown is **automatically selected** to match your code.
-
+
Click **Submit**, confirm the dialog, and your code & language are auto-filled.
-
-
-
-
-
-
-
-
Back to Features
---
@@ -297,11 +298,12 @@ Provides a **Stopwatch** and a **Countdown Timer**.
- **Auto Complete ON/OFF**: Toggle code auto-completion popups
- **Inspections ON/OFF**: Enable power save mode to stop background analysis
+- **Code Vision ON/OFF**: Hide "N usages" and other Code Vision hints in the editor
- **Paste Block**: Block pasting text copied from external programs
- **Focus Alert**: Show alert when IDE window loses focus
- **Language**: Switch between Korean / English
-One-click **Exam Mode** enables all 4 restrictions; **Normal Mode** disables them all.
+One-click **Exam Mode** enables all 5 restrictions; **Normal Mode** disables them all.
@@ -357,8 +359,8 @@ Switch between **Korean / English** in settings. All UI text is displayed in the
## Requirements
-- IntelliJ IDEA 2024.1+
-- JDK 17+ (for Java execution)
+- IntelliJ IDEA 2024.3+
+- JDK 17+ (for Java execution) — building the plugin from source requires JDK 21
- Language compilers (for respective language tests)
## Build
@@ -409,6 +411,12 @@ CodingTestKit은 **실제 시험 환경을 IDE 안에서 그대로 재현**하
| Python | O | O | O | O |
| C++ | O | O | O | O |
| Kotlin | O | X | O | O |
+| JavaScript | O | X | O | O |
+| Rust | X | X | O | O |
+| Go | O | X | O | O |
+| Ruby | O | X | O | O |
+
+> X = 해당 플랫폼 제출 미지원. 로컬 테스트 실행은 모든 언어 지원 (컴파일러/런타임 설치 필요: JDK, python3, g++, kotlinc, Node.js, rustc, go, ruby). 제출 미지원 조합을 선택하면 플랫폼/언어 콤보 옆에 즉시 경고가 표시됩니다.
---
@@ -424,12 +432,13 @@ CodingTestKit은 **실제 시험 환경을 IDE 안에서 그대로 재현**하
| 4 | [**로그인 & 제출**](docs/features/submit.md) | 내장 브라우저로 코드 제출 + 언어 자동 선택 |
| 5 | [**문제 검색**](docs/features/search.md) | LeetCode, Codeforces, 프로그래머스, SWEA 문제 검색 |
| 6 | [**랜덤 문제 뽑기**](docs/features/random.md) | 티어/난이도/태그 필터로 랜덤 문제 추천 |
-| 7 | [**코드 에디터**](#코드-에디터) | 플랫폼 & 언어별 보일러플레이트 코드 자동 생성 |
-| 8 | [**코드 템플릿**](docs/features/templates.md) | 자주 쓰는 코드 스니펫 저장 & 재사용 |
-| 9 | [**타이머**](docs/features/timer.md) | 스톱워치 + 원형 다이얼/프로그레스 바/디지털 시계 카운트다운 |
-| 10 | [**설정 & 시험 모드**](docs/features/exam-mode.md) | 원클릭 시험 모드: 붙여넣기 차단, 자동완성 끄기, 포커스 감지 |
-| 11 | [**GitHub 연동**](docs/features/github.md) | 맞은 문제 자동 GitHub 푸시 |
-| 12 | [**다국어 지원**](#다국어-지원-i18n) | 한국어 / English UI 완전 지원 |
+| 7 | **내 풀이** | LeetCode & Codeforces에서 내가 푼 문제 목록 확인 |
+| 8 | [**코드 에디터**](#코드-에디터) | 플랫폼 & 언어별 보일러플레이트 코드 자동 생성 |
+| 9 | [**코드 템플릿**](docs/features/templates.md) | 자주 쓰는 코드 스니펫 저장 & 재사용 |
+| 10 | [**타이머**](docs/features/timer.md) | 스톱워치 + 원형 다이얼/프로그레스 바/디지털 시계 카운트다운 |
+| 11 | [**설정 & 시험 모드**](docs/features/exam-mode.md) | 원클릭 시험 모드: 붙여넣기 차단, 자동완성 끄기, 포커스 감지 |
+| 12 | [**GitHub 연동**](docs/features/github.md) | 맞은 문제 자동 GitHub 푸시 |
+| 13 | [**다국어 지원**](#다국어-지원-i18n) | 한국어 / English UI 완전 지원 |
---
@@ -442,6 +451,8 @@ CodingTestKit은 **실제 시험 환경을 IDE 안에서 그대로 재현**하
- **LeetCode**: 문제 번호, slug, 또는 URL 입력 (예: `1`, `two-sum`, URL)
- **Codeforces**: 콘테스트번호+문제번호 (예: `1234A`) 또는 URL
+> Codeforces: Cloudflare가 요청을 차단하면 내장 JCEF 브라우저로 자동 폴백합니다 (약 5~20초 더 걸릴 수 있음).
+
@@ -463,7 +474,7 @@ CodingTestKit은 **실제 시험 환경을 IDE 안에서 그대로 재현**하
### 문제 보기 & 번역
-
+
플러그인 패널에서 문제 설명, 입출력 형식, 예제를 바로 확인할 수 있습니다.
@@ -534,19 +545,11 @@ FAIL인 케이스는 빨간색으로 표시되어 한눈에 확인할 수 있습
내장 JCEF 브라우저를 통해 각 플랫폼에 로그인하고, 코드를 직접 제출할 수 있습니다. 제출 시 코드 작성 언어에 맞춰 **언어 드롭다운이 자동 선택**됩니다.
-
+
**제출** 버튼을 누르면 제출 확인 다이얼로그가 표시되고, 코드와 언어가 자동으로 입력됩니다.
-
-
-
-
-
-
-
-
기능 목록으로
---
@@ -638,11 +641,12 @@ FAIL인 케이스는 빨간색으로 표시되어 한눈에 확인할 수 있습
- **자동완성 ON/OFF**: 코드 자동완성 팝업을 끄고 켤 수 있습니다
- **코드 검사 ON/OFF**: 절전 모드를 활성화하여 백그라운드 분석을 중지합니다
+- **사용 위치 힌트 끄기 (Code Vision)**: 에디터의 'N개 사용 위치' 등 Code Vision 힌트를 숨깁니다
- **외부 붙여넣기 차단**: 외부 프로그램에서 복사한 텍스트의 붙여넣기를 차단합니다
- **포커스 이탈 감지**: IDE 창에서 포커스가 벗어나면 경고를 표시합니다
- **언어 설정**: 한국어 / English 전환 가능
-**시험 모드** 버튼을 누르면 4가지 설정이 한 번에 적용되고, **일반 모드** 버튼을 누르면 모두 해제됩니다.
+**시험 모드** 버튼을 누르면 5가지 설정이 한 번에 적용되고, **일반 모드** 버튼을 누르면 모두 해제됩니다.
@@ -698,8 +702,8 @@ FAIL인 케이스는 빨간색으로 표시되어 한눈에 확인할 수 있습
## 요구 사항
-- IntelliJ IDEA 2024.1 이상
-- JDK 17 이상 (Java 실행용)
+- IntelliJ IDEA 2024.3 이상
+- JDK 17 이상 (Java 실행용) — 플러그인을 소스에서 직접 빌드하려면 JDK 21 필요
- 각 언어 컴파일러 (해당 언어 테스트 시)
## 빌드
diff --git a/build.gradle.kts b/build.gradle.kts
index 2c91449..c69761c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -5,7 +5,7 @@ plugins {
}
group = "com.codingtestkit"
-version = "1.4.4"
+version = "1.5.0"
repositories {
mavenCentral()
@@ -33,7 +33,7 @@ intellijPlatform {
pluginConfiguration {
id = "com.codingtestkit"
name = "CodingTestKit"
- version = "1.4.4"
+ version = "1.5.0"
ideaVersion {
sinceBuild = "243"
untilBuild = provider { null }
diff --git a/docs/README_en.md b/docs/README_en.md
index 2247691..cfe952e 100644
--- a/docs/README_en.md
+++ b/docs/README_en.md
@@ -42,6 +42,12 @@ CodingTestKit was built to **replicate the real exam environment inside your IDE
| Python | O | O | O | O |
| C++ | O | O | O | O |
| Kotlin | O | X | O | O |
+| JavaScript | O | X | O | O |
+| Rust | X | X | O | O |
+| Go | O | X | O | O |
+| Ruby | O | X | O | O |
+
+> X = submission not supported on that platform. Local test execution works for all languages (compiler/runtime must be installed: JDK, python3, g++, kotlinc, Node.js, rustc, go, ruby). Selecting an unsupported combination shows a warning next to the platform/language selectors immediately.
---
@@ -49,7 +55,7 @@ CodingTestKit was built to **replicate the real exam environment inside your IDE
### Internationalization (i18n)
-Switch between **Korean / English** in settings. All UI text is displayed in the selected language. The language change note is shown in both languages simultaneously.
+Switch between **Korean / English** in settings. All UI text is displayed in the selected language. A note reminds you to reopen the tool window to apply changes.
### Fetch Problems
@@ -60,6 +66,8 @@ Select the platform and language, enter a problem number, and the problem descri
- **LeetCode**: Enter number, slug, or URL (e.g., `1`, `two-sum`, full URL)
- **Codeforces**: contestId+letter (e.g., `1234A`) or URL
+> Codeforces: if Cloudflare blocks the request, fetching automatically falls back to the built-in JCEF browser (may take ~5–20 seconds longer).
+
@@ -82,7 +90,7 @@ When a problem is fetched, a folder is automatically created with a code file an
### Problem View & Translation
-
+
View the problem description, I/O format, and examples directly in the plugin panel.
@@ -146,11 +154,7 @@ Log in to each platform via the built-in JCEF browser and submit your code direc
Click **Submit**, confirm the dialog, and your code & language are auto-filled.
-
-
-
-
-
+
Login and submission work the same way for Programmers, SWEA, LeetCode, and Codeforces.
@@ -227,11 +231,12 @@ Practice under conditions identical to real coding tests.
- **Auto Complete ON/OFF**: Toggle code auto-completion popups
- **Inspections ON/OFF**: Enable power save mode to stop background analysis
+- **Code Vision ON/OFF**: Hide "N usages" and other Code Vision hints in the editor
- **Paste Block**: Block pasting text copied from external programs (internal copy/paste works normally)
- **Focus Alert**: Show alert when IDE window loses focus (same as cheating detection in real tests)
-- **Language**: Switch between Korean / English (bilingual note)
+- **Language**: Switch between Korean / English (a note reminds you to reopen the tool window to apply changes)
-One-click **Exam Mode** enables all 4 restrictions; **Normal Mode** disables them all.
+One-click **Exam Mode** enables all 5 restrictions; **Normal Mode** disables them all.
@@ -321,8 +326,8 @@ Setup: Settings > GitHub Integration > Click "GitHub Login" and select your repo
## Requirements
-- IntelliJ IDEA 2024.1+
-- JDK 17+ (for Java execution)
+- IntelliJ IDEA 2024.3+
+- JDK 17+ (for Java execution) — building the plugin from source requires JDK 21
- Language compilers (for respective language tests)
## Build
diff --git a/docs/README_ko.md b/docs/README_ko.md
index 91b1786..7a87f89 100644
--- a/docs/README_ko.md
+++ b/docs/README_ko.md
@@ -44,6 +44,12 @@ CodingTestKit은 이런 **실제 시험 환경을 IDE 안에서 그대로 재현
| Python | O | O | O | O |
| C++ | O | O | O | O |
| Kotlin | O | X | O | O |
+| JavaScript | O | X | O | O |
+| Rust | X | X | O | O |
+| Go | O | X | O | O |
+| Ruby | O | X | O | O |
+
+> X = 해당 플랫폼 제출 미지원. 로컬 테스트 실행은 모든 언어 지원 (컴파일러/런타임 설치 필요: JDK, python3, g++, kotlinc, Node.js, rustc, go, ruby). 제출 미지원 조합을 선택하면 플랫폼/언어 콤보 옆에 즉시 경고가 표시됩니다.
---
@@ -51,7 +57,7 @@ CodingTestKit은 이런 **실제 시험 환경을 IDE 안에서 그대로 재현
### 다국어 지원 (i18n)
-설정에서 **한국어 / English** 전환이 가능합니다. 모든 UI 텍스트가 선택한 언어로 표시됩니다. 언어 변경 안내문은 한·영 동시에 표시됩니다.
+설정에서 **한국어 / English** 전환이 가능합니다. 모든 UI 텍스트가 선택한 언어로 표시됩니다. 언어 변경 안내문은 선택된 언어로 표시되며, 툴 윈도우를 다시 열면 적용됩니다.
### 문제 가져오기
@@ -62,6 +68,8 @@ CodingTestKit은 이런 **실제 시험 환경을 IDE 안에서 그대로 재현
- **LeetCode**: 문제 번호, slug, 또는 URL 입력 (예: `1`, `two-sum`, URL)
- **Codeforces**: 콘테스트번호+문제번호 (예: `1234A`) 또는 URL
+> Codeforces: Cloudflare가 요청을 차단하면 내장 JCEF 브라우저로 자동 폴백합니다 (약 5~20초 더 걸릴 수 있음).
+
@@ -84,7 +92,7 @@ CodingTestKit은 이런 **실제 시험 환경을 IDE 안에서 그대로 재현
### 문제 보기 & 번역
-
+
플러그인 패널에서 문제 설명, 입출력 형식, 예제를 바로 확인할 수 있습니다.
@@ -147,6 +155,10 @@ LeetCode 문제를 키워드, 난이도, 알고리즘 태그로 검색합니다.
**제출** 버튼을 누르면 내장 브라우저에서 제출 페이지가 열리고, 코드와 언어가 자동으로 입력됩니다. 사용자는 스크롤만 내려서 **제출** 버튼을 누르면 끝입니다.
+
+
+
+
**프로그래머스**, **SWEA**, **LeetCode**, **Codeforces** 모두 동일한 방식으로 로그인과 제출이 가능합니다.
### 로컬 테스트 실행
@@ -221,11 +233,12 @@ FAIL인 케이스는 빨간색으로 표시되어 한눈에 확인할 수 있습
- **자동완성 ON/OFF**: 코드 자동완성 팝업을 끄고 켤 수 있습니다
- **코드 검사 ON/OFF**: 절전 모드를 활성화하여 백그라운드 분석을 중지합니다
+- **사용 위치 힌트 끄기 (Code Vision)**: 에디터의 'N개 사용 위치' 등 Code Vision 힌트를 숨깁니다
- **외부 붙여넣기 차단**: 외부 프로그램에서 복사한 텍스트의 붙여넣기를 차단합니다
- **포커스 이탈 감지**: IDE 창에서 포커스가 벗어나면 경고를 표시합니다
-- **언어 설정**: 한국어 / English 전환 가능 (안내문 한·영 동시 표시)
+- **언어 설정**: 한국어 / English 전환 가능 (안내문은 선택된 언어로 표시)
-**시험 모드** 버튼을 누르면 4가지 설정이 한 번에 적용되고, **일반 모드** 버튼을 누르면 모두 해제됩니다.
+**시험 모드** 버튼을 누르면 5가지 설정이 한 번에 적용되고, **일반 모드** 버튼을 누르면 모두 해제됩니다.
@@ -315,8 +328,8 @@ FAIL인 케이스는 빨간색으로 표시되어 한눈에 확인할 수 있습
## 요구 사항
-- IntelliJ IDEA 2024.1 이상
-- JDK 17 이상 (Java 실행용)
+- IntelliJ IDEA 2024.3 이상
+- JDK 17 이상 (Java 실행용) — 플러그인을 소스에서 직접 빌드하려면 JDK 21 필요
- 각 언어 컴파일러 (해당 언어 테스트 시)
## 빌드
diff --git a/docs/features/exam-mode.md b/docs/features/exam-mode.md
index 06630b3..743740d 100644
--- a/docs/features/exam-mode.md
+++ b/docs/features/exam-mode.md
@@ -16,10 +16,11 @@ Simulate real coding test environments by restricting IDE features.
|---------|-------------|
| **Auto Complete OFF** | Disables code auto-completion popups |
| **Inspections OFF** | Enables power save mode to stop background analysis |
+| **Code Vision OFF** | Hides "N usages" and other Code Vision hints in the editor |
| **Paste Block** | Blocks pasting text copied from external programs |
| **Focus Alert** | Shows alert when IDE window loses focus |
-One-click **Exam Mode** enables all 4 restrictions simultaneously.
+One-click **Exam Mode** enables all 5 restrictions simultaneously.
One-click **Normal Mode** disables them all.
@@ -52,10 +53,11 @@ One-click **Normal Mode** disables them all.
|------|------|
| **자동완성 OFF** | 코드 자동완성 팝업을 끕니다 |
| **코드 검사 OFF** | 절전 모드를 활성화하여 백그라운드 분석을 중지합니다 |
+| **사용 위치 힌트 끄기 (Code Vision OFF)** | 에디터의 'N개 사용 위치' 등 Code Vision 힌트를 숨깁니다 |
| **외부 붙여넣기 차단** | 외부 프로그램에서 복사한 텍스트의 붙여넣기를 차단합니다 |
| **포커스 이탈 감지** | IDE 창에서 포커스가 벗어나면 경고를 표시합니다 |
-**시험 모드** 버튼 → 4가지 설정 한 번에 적용
+**시험 모드** 버튼 → 5가지 설정 한 번에 적용
**일반 모드** 버튼 → 모두 해제
### 기타 설정
diff --git a/docs/features/fetch.md b/docs/features/fetch.md
index 3fde438..2115b64 100644
--- a/docs/features/fetch.md
+++ b/docs/features/fetch.md
@@ -21,8 +21,8 @@ Select the platform and language, enter a problem number, and the problem descri
### What Happens After Fetch
-1. A **folder** is created under your project: `[Platform] ProblemNumber - Title/`
-2. A **code file** with boilerplate is generated (e.g., `Main.java`, `solution.py`)
+1. A **folder** is created under your project: `problems/{platform}/{difficulty}/{id}. {title}/` (e.g., `problems/LeetCode/Medium/1. Two Sum/`; `Unrated` is used when the difficulty is unknown)
+2. A **code file** with boilerplate is generated — `Solution.java` / `Solution.py` etc. for Programmers, SWEA, and LeetCode; `Main.*` for Codeforces
3. A **README.md** with the problem description is optionally created (toggle in Settings)
4. **Test cases** are automatically extracted and ready to run
@@ -55,6 +55,11 @@ Select the platform and language, enter a problem number, and the problem descri
- Extracts code snippets, example test cases, and metadata
- Supports number (`1`), slug (`two-sum`), and full URL input
+#### Codeforces
+- Fetches via HTTP crawling first; if Cloudflare blocks the request (e.g., 403), automatically falls back to the built-in offscreen JCEF browser, which passes the challenge and extracts the page
+- When the fallback engages, fetching may take about 5–20 seconds longer
+- Requires an IDE with JCEF support (all recent IntelliJ versions)
+
---
## 한국어
@@ -72,8 +77,8 @@ Select the platform and language, enter a problem number, and the problem descri
### 가져오기 후 결과
-1. 프로젝트 내에 **폴더**가 생성됩니다: `[플랫폼] 문제번호 - 제목/`
-2. 보일러플레이트가 포함된 **코드 파일**이 생성됩니다 (예: `Main.java`, `solution.py`)
+1. 프로젝트 내에 **폴더**가 생성됩니다: `problems/{플랫폼}/{난이도}/{문제번호}. {제목}/` (예: `problems/LeetCode/Medium/1. Two Sum/`, 난이도를 알 수 없으면 `Unrated`)
+2. 보일러플레이트가 포함된 **코드 파일**이 생성됩니다 — 프로그래머스·SWEA·LeetCode는 `Solution.java` / `Solution.py` 등, Codeforces는 `Main.*`
3. 문제 설명이 담긴 **README.md**가 선택적으로 생성됩니다 (설정에서 토글 가능)
4. **테스트 케이스**가 자동 추출되어 바로 실행 가능합니다
@@ -93,3 +98,8 @@ Select the platform and language, enter a problem number, and the problem descri
- GraphQL API로 가져옴
- 코드 스니펫, 예제 테스트 케이스, 메타데이터 추출
- 번호(`1`), slug(`two-sum`), 전체 URL 입력 지원
+
+#### Codeforces
+- HTTP 크롤링으로 먼저 가져오고, Cloudflare에 차단(403 등)되면 내장 오프스크린 JCEF 브라우저로 자동 폴백하여 챌린지를 통과한 뒤 페이지를 추출
+- 폴백이 동작하면 가져오기가 약 5~20초 더 걸릴 수 있음
+- JCEF를 지원하는 IDE 필요 (최근 IntelliJ 버전은 모두 지원)
diff --git a/docs/features/submit.md b/docs/features/submit.md
index ecc3b3a..73d2c9b 100644
--- a/docs/features/submit.md
+++ b/docs/features/submit.md
@@ -15,7 +15,7 @@ Log in to each platform via the built-in JCEF browser and submit your code direc
Click the **Login** button for any platform. A built-in browser opens the platform's login page.
-
+
### Submit Flow
@@ -26,13 +26,13 @@ Click the **Login** button for any platform. A built-in browser opens the platfo
4. **Language** dropdown is automatically selected to match your code
5. Verify and click the platform's submit button
-
-
-
+### Unsupported Combination Warning
-
-
-
+Some (platform, language) combinations cannot be submitted (e.g., Rust on Programmers; Kotlin, JavaScript, Rust, Go, or Ruby on SWEA). When you select such a combination, a warning appears next to the platform/language selectors immediately:
+
+> ⚠ Rust — cannot submit to Programmers (local testing only)
+
+Local test execution still works for every combination — only submission is restricted.
### Auto Language Selection
@@ -75,6 +75,14 @@ After submitting, the plugin monitors the page for verdict:
4. **언어** 드롭다운이 코드 작성 언어에 맞춰 자동 선택
5. 확인 후 플랫폼의 제출 버튼 클릭
+### 제출 미지원 조합 경고
+
+일부 (플랫폼, 언어) 조합은 제출이 불가능합니다 (예: 프로그래머스 × Rust, SWEA × Kotlin/JavaScript/Rust/Go/Ruby). 이런 조합을 선택하면 플랫폼/언어 콤보 옆에 즉시 경고가 표시됩니다:
+
+> ⚠ Rust — 프로그래머스 제출 미지원 (로컬 테스트만 가능)
+
+로컬 테스트 실행은 모든 조합에서 가능하며, 제출만 제한됩니다.
+
### 언어 자동 선택
모든 플랫폼에서 언어 드롭다운이 자동 설정됩니다:
diff --git a/docs/features/test.md b/docs/features/test.md
index cc95369..48102b0 100644
--- a/docs/features/test.md
+++ b/docs/features/test.md
@@ -47,6 +47,10 @@ Failed cases are highlighted in red and auto-expanded for easy debugging.
+### Language Selection
+
+The language selector in the Tests tab is synchronized with the one in the Problems tab — changing either one automatically updates the other.
+
### Supported Languages
| Language | Compiler/Runtime | Notes |
@@ -56,6 +60,9 @@ Failed cases are highlighted in red and auto-expanded for easy debugging.
| C++ | `g++` | C++17 standard |
| Kotlin | `kotlinc` + `kotlin` | JVM-based execution |
| JavaScript | `node` | Node.js runtime |
+| Rust | `rustc` | Install via rustup — auto-detects `~/.cargo/bin` |
+| Go | `go` | Install from go.dev/dl — auto-detects `/usr/local/go/bin`, `/opt/homebrew` |
+| Ruby | `ruby` | Install via `brew install ruby` or rbenv — auto-detects `/opt/homebrew`, rbenv shims |
### Comparison Logic
@@ -87,6 +94,10 @@ Failed cases are highlighted in red and auto-expanded for easy debugging.
- 클래스 기반(LeetCode)과 순수 함수(프로그래머스) 패턴 자동 감지
- 각 언어에 맞는 드라이버 코드 자동 생성
+### 언어 선택
+
+테스트 탭의 언어 콤보는 문제 탭의 언어 콤보와 양방향으로 동기화됩니다 — 한쪽에서 언어를 바꾸면 다른 쪽도 자동으로 바뀝니다.
+
### 지원 언어
| 언어 | 컴파일러/런타임 | 비고 |
@@ -96,6 +107,9 @@ Failed cases are highlighted in red and auto-expanded for easy debugging.
| C++ | `g++` | C++17 표준 |
| Kotlin | `kotlinc` + `kotlin` | JVM 기반 실행 |
| JavaScript | `node` | Node.js 런타임 |
+| Rust | `rustc` | rustup으로 설치 — `~/.cargo/bin` 자동 감지 |
+| Go | `go` | go.dev/dl에서 설치 — `/usr/local/go/bin`, `/opt/homebrew` 자동 감지 |
+| Ruby | `ruby` | `brew install ruby` 또는 rbenv로 설치 — `/opt/homebrew`, rbenv shims 자동 감지 |
### 비교 로직
diff --git a/docs/screenshots/boj-login.png b/docs/screenshots/boj-login.png
deleted file mode 100644
index 5bf0add..0000000
Binary files a/docs/screenshots/boj-login.png and /dev/null differ
diff --git a/docs/screenshots/boj-submit-code.png b/docs/screenshots/boj-submit-code.png
deleted file mode 100644
index f405172..0000000
Binary files a/docs/screenshots/boj-submit-code.png and /dev/null differ
diff --git a/docs/screenshots/boj-submit-confirm.png b/docs/screenshots/boj-submit-confirm.png
deleted file mode 100644
index c776a32..0000000
Binary files a/docs/screenshots/boj-submit-confirm.png and /dev/null differ
diff --git a/docs/screenshots/boj-submit-success.png b/docs/screenshots/boj-submit-success.png
deleted file mode 100644
index a2bfa93..0000000
Binary files a/docs/screenshots/boj-submit-success.png and /dev/null differ
diff --git a/src/main/kotlin/com/codingtestkit/model/Models.kt b/src/main/kotlin/com/codingtestkit/model/Models.kt
index 28958ab..34716c5 100644
--- a/src/main/kotlin/com/codingtestkit/model/Models.kt
+++ b/src/main/kotlin/com/codingtestkit/model/Models.kt
@@ -58,7 +58,21 @@ enum class Language(
PYTHON("Python", "py", 5),
CPP("C++", "cpp", 1),
KOTLIN("Kotlin", "kt", -1),
- JAVASCRIPT("JavaScript", "js", -1);
+ JAVASCRIPT("JavaScript", "js", -1),
+ RUST("Rust", "rs", -1),
+ GO("Go", "go", -1),
+ RUBY("Ruby", "rb", -1);
+
+ /**
+ * 해당 플랫폼에 코드를 제출할 수 있는 언어인지.
+ * 로컬 테스트 실행은 언어·플랫폼 무관 항상 가능하며, 제출만 플랫폼 제약을 받는다.
+ */
+ fun isSubmittable(source: ProblemSource): Boolean = when (source) {
+ ProblemSource.PROGRAMMERS -> this != RUST // 프로그래머스는 Rust 채점 미지원
+ ProblemSource.SWEA -> sweaId >= 0
+ ProblemSource.LEETCODE -> true
+ ProblemSource.CODEFORCES -> true
+ }
fun defaultCode(source: ProblemSource): String {
return when (this) {
@@ -186,6 +200,102 @@ enum class Language(
|});
""".trimMargin()
}
+ RUST -> when (source) {
+ ProblemSource.PROGRAMMERS -> """
+ |fn solution() -> i32 {
+ | let answer = 0;
+ | answer
+ |}
+ """.trimMargin()
+ ProblemSource.SWEA -> """
+ |use std::io::{self, BufRead};
+ |
+ |fn main() {
+ | let stdin = io::stdin();
+ | let mut lines = stdin.lock().lines();
+ | let t: usize = lines.next().unwrap().unwrap().trim().parse().unwrap();
+ | for tc in 1..=t {
+ | println!("#{} ", tc);
+ | }
+ |}
+ """.trimMargin()
+ ProblemSource.LEETCODE -> ""
+ ProblemSource.CODEFORCES -> """
+ |use std::io::{self, Read};
+ |
+ |fn main() {
+ | let mut input = String::new();
+ | io::stdin().read_to_string(&mut input).unwrap();
+ | let mut it = input.split_whitespace();
+ |
+ |}
+ """.trimMargin()
+ }
+ GO -> when (source) {
+ ProblemSource.PROGRAMMERS -> """
+ |func solution() int {
+ | answer := 0
+ | return answer
+ |}
+ """.trimMargin()
+ ProblemSource.SWEA -> """
+ |package main
+ |
+ |import (
+ | "bufio"
+ | "fmt"
+ | "os"
+ |)
+ |
+ |func main() {
+ | reader := bufio.NewReader(os.Stdin)
+ | var T int
+ | fmt.Fscan(reader, &T)
+ | for tc := 1; tc <= T; tc++ {
+ | fmt.Printf("#%d \n", tc)
+ | }
+ |}
+ """.trimMargin()
+ ProblemSource.LEETCODE -> ""
+ ProblemSource.CODEFORCES -> """
+ |package main
+ |
+ |import (
+ | "bufio"
+ | "fmt"
+ | "os"
+ |)
+ |
+ |func main() {
+ | reader := bufio.NewReader(os.Stdin)
+ | writer := bufio.NewWriter(os.Stdout)
+ | defer writer.Flush()
+ |
+ | var n int
+ | fmt.Fscan(reader, &n)
+ | fmt.Fprintln(writer, n)
+ |}
+ """.trimMargin()
+ }
+ RUBY -> when (source) {
+ ProblemSource.PROGRAMMERS -> """
+ |def solution()
+ | answer = 0
+ | answer
+ |end
+ """.trimMargin()
+ ProblemSource.SWEA -> """
+ |T = gets.to_i
+ |(1..T).each do |tc|
+ | puts "##{tc} "
+ |end
+ """.trimMargin()
+ ProblemSource.LEETCODE -> ""
+ ProblemSource.CODEFORCES -> """
+ |lines = STDIN.read.split("\n")
+ |
+ """.trimMargin()
+ }
}
}
}
diff --git a/src/main/kotlin/com/codingtestkit/service/AuthService.kt b/src/main/kotlin/com/codingtestkit/service/AuthService.kt
index 6ce717a..bf56a9c 100644
--- a/src/main/kotlin/com/codingtestkit/service/AuthService.kt
+++ b/src/main/kotlin/com/codingtestkit/service/AuthService.kt
@@ -33,11 +33,31 @@ class AuthService : PersistentStateComponent {
private var authState = AuthState()
+ /**
+ * PasswordSafe(OS 키체인) 접근은 느려서 EDT에서 호출하면 SlowOperations 위반.
+ * 읽기는 메모리 캐시로 즉시 응답하고, 쓰기는 캐시 갱신 후 단일 스레드로 영속화한다
+ * (단일 스레드 executor라 쓰기 순서가 보장됨).
+ */
+ private val cookieCache = java.util.concurrent.ConcurrentHashMap()
+ private val persistExecutor = java.util.concurrent.Executors.newSingleThreadExecutor { r ->
+ Thread(r, "CodingTestKit-AuthPersist").apply { isDaemon = true }
+ }
+
+ init {
+ // 캐시 예열: 이후 EDT에서의 readCookies가 키체인을 건드리지 않게 함
+ persistExecutor.execute {
+ for (source in ProblemSource.entries) {
+ val stored = PasswordSafe.instance.getPassword(credentialAttributes(source)) ?: ""
+ cookieCache.putIfAbsent(source, stored)
+ }
+ }
+ }
+
override fun getState(): AuthState = authState
override fun loadState(state: AuthState) {
authState = state
- migrateLegacyCookies()
+ persistExecutor.execute { migrateLegacyCookies() }
}
private fun migrateLegacyCookies() {
@@ -63,11 +83,17 @@ class AuthService : PersistentStateComponent {
CredentialAttributes(generateServiceName("CodingTestKit", "cookie.${source.name}"))
private fun writeCookies(source: ProblemSource, cookies: String) {
- PasswordSafe.instance.setPassword(credentialAttributes(source), cookies.ifBlank { null })
+ cookieCache[source] = cookies
+ persistExecutor.execute {
+ PasswordSafe.instance.setPassword(credentialAttributes(source), cookies.ifBlank { null })
+ }
}
private fun readCookies(source: ProblemSource): String =
- PasswordSafe.instance.getPassword(credentialAttributes(source)) ?: ""
+ cookieCache.getOrPut(source) {
+ // 캐시 예열 전에 호출된 경우의 폴백 (드묾)
+ PasswordSafe.instance.getPassword(credentialAttributes(source)) ?: ""
+ }
fun getCookies(source: ProblemSource): String = readCookies(source)
diff --git a/src/main/kotlin/com/codingtestkit/service/CodeRunner.kt b/src/main/kotlin/com/codingtestkit/service/CodeRunner.kt
index a639235..40d748d 100644
--- a/src/main/kotlin/com/codingtestkit/service/CodeRunner.kt
+++ b/src/main/kotlin/com/codingtestkit/service/CodeRunner.kt
@@ -35,6 +35,9 @@ object CodeRunner {
Language.CPP -> runCpp(code, testCase.input, tempDir, timeoutSeconds)
Language.KOTLIN -> runKotlin(code, testCase.input, tempDir, timeoutSeconds)
Language.JAVASCRIPT -> runJavaScript(code, testCase.input, tempDir, timeoutSeconds)
+ Language.RUST -> runRust(code, testCase.input, tempDir, timeoutSeconds)
+ Language.GO -> runGo(code, testCase.input, tempDir, timeoutSeconds)
+ Language.RUBY -> runRuby(code, testCase.input, tempDir, timeoutSeconds)
}
} catch (e: Exception) {
RunResult(output = "", error = e.message ?: "Unknown error", exitCode = -1)
@@ -69,6 +72,9 @@ object CodeRunner {
Language.CPP -> wrapCpp(code, inputValues, parameterNames)
Language.KOTLIN -> wrapKotlin(code, inputValues, parameterNames)
Language.JAVASCRIPT -> wrapJavaScript(code, inputValues, parameterNames)
+ Language.RUST -> wrapRust(code, inputValues, parameterNames)
+ Language.GO -> wrapGo(code, inputValues, parameterNames)
+ Language.RUBY -> wrapRuby(code, inputValues, parameterNames)
}
val tempDir = createTempDir()
@@ -79,6 +85,9 @@ object CodeRunner {
Language.CPP -> runCpp(wrappedCode, "", tempDir, timeoutSeconds)
Language.KOTLIN -> runKotlin(wrappedCode, "", tempDir, timeoutSeconds)
Language.JAVASCRIPT -> runJavaScript(wrappedCode, "", tempDir, timeoutSeconds)
+ Language.RUST -> runRust(wrappedCode, "", tempDir, timeoutSeconds)
+ Language.GO -> runGo(wrappedCode, "", tempDir, timeoutSeconds)
+ Language.RUBY -> runRuby(wrappedCode, "", tempDir, timeoutSeconds)
}
} catch (e: Exception) {
RunResult(output = "", error = e.message ?: "Unknown error", exitCode = -1)
@@ -94,6 +103,10 @@ object CodeRunner {
Language.CPP -> code.contains("int main")
Language.KOTLIN -> code.contains("fun main")
Language.JAVASCRIPT -> code.contains("readline") || code.contains("process.stdin")
+ Language.RUST -> code.contains("fn main")
+ Language.GO -> code.contains("func main")
+ // Ruby는 main이 없으므로 stdin 사용 여부로 스크립트/함수 스타일을 구분
+ Language.RUBY -> code.contains("gets") || code.contains("STDIN")
}
}
@@ -447,6 +460,160 @@ else console.log(_result);
""".trimIndent()
}
+ /**
+ * Rust 배열 변환: [1,2,3] → vec![1,2,3], 문자열 → String::from("...")
+ */
+ private fun toRustLiteral(value: String): String {
+ val v = value.trim()
+ if (v.startsWith("\"")) return "String::from($v)"
+ if (!v.startsWith("[")) return v
+
+ if (v.startsWith("[[")) {
+ val inner = v.removePrefix("[").removeSuffix("]")
+ val arrays = mutableListOf()
+ var depth = 0
+ var current = StringBuilder()
+ for (c in inner) {
+ if (c == '[') depth++
+ if (c == ']') depth--
+ current.append(c)
+ if (depth == 0 && current.isNotBlank()) {
+ val arr = current.toString().trim().removePrefix(",").trim()
+ if (arr.isNotBlank()) arrays.add(arr)
+ current = StringBuilder()
+ }
+ }
+ val converted = arrays.joinToString(", ") { toRustLiteral(it) }
+ return "vec![$converted]"
+ }
+
+ val content = v.removePrefix("[").removeSuffix("]").trim()
+ if (content.isEmpty()) return "vec![]"
+ val first = content.split(",").firstOrNull()?.trim() ?: ""
+ return if (first.startsWith("\"")) {
+ val items = content.split(",").joinToString(", ") { "String::from(${it.trim()})" }
+ "vec![$items]"
+ } else {
+ "vec![$content]"
+ }
+ }
+
+ /**
+ * Go 배열 변환: [1,2,3] → []int{1,2,3}, [[1,2],[3,4]] → [][]int{{1,2},{3,4}}
+ */
+ private fun toGoLiteral(value: String): String {
+ val v = value.trim()
+ if (!v.startsWith("[")) return v
+
+ if (v.startsWith("[[")) {
+ // 첫 내부 배열의 원소로 타입 감지: [[1,2],[3,4]] → [][]int{{1,2},{3,4}}
+ val firstInner = v.removePrefix("[").substringAfter("[").substringBefore("]")
+ val elemType = detectGoElementType(firstInner)
+ return "[][]$elemType" + v.replace('[', '{').replace(']', '}')
+ }
+
+ val content = v.removePrefix("[").removeSuffix("]").trim()
+ if (content.isEmpty()) return "[]int{}"
+ val elemType = detectGoElementType(content)
+ return "[]$elemType{$content}"
+ }
+
+ private fun detectGoElementType(content: String): String {
+ val first = content.split(",").firstOrNull()?.trim() ?: ""
+ return when {
+ first.startsWith("\"") -> "string"
+ first == "true" || first == "false" -> "bool"
+ first.contains(".") -> "float64"
+ first.toLongOrNull() != null && (first.toLong() > Int.MAX_VALUE || first.toLong() < Int.MIN_VALUE) -> "int64"
+ else -> "int"
+ }
+ }
+
+ private fun wrapRust(code: String, inputValues: List, @Suppress("UNUSED_PARAMETER") paramNames: List): String {
+ val args = inputValues.joinToString(", ") { toRustLiteral(it) }
+
+ // fn 메서드명 추출 (solution 우선, 없으면 마지막)
+ val rsFns = Regex("""fn\s+(\w+)\s*\(""").findAll(code)
+ .map { it.groupValues[1] }
+ .filter { it != "main" }.toList()
+ val methodName = rsFns.find { it == "solution" } ?: rsFns.lastOrNull() ?: "solution"
+
+ // LeetCode 스타일(impl Solution)이면 struct 선언 보충 후 연관 함수로 호출
+ val hasImpl = code.contains("impl Solution")
+ val structDecl = if (hasImpl && !code.contains("struct Solution")) "struct Solution;\n\n" else ""
+ val callExpr = if (hasImpl) "Solution::$methodName($args)" else "$methodName($args)"
+
+ return """
+$structDecl$code
+
+fn main() {
+ let _result = $callExpr;
+ // {:?}는 문자열을 "따옴표 포함"으로, 벡터를 [a, b]로 출력 → 공백 제거로 [a,b] 형태 통일
+ let _s = format!("{:?}", _result).replace(", ", ",");
+ println!("{}", _s);
+}
+""".trimIndent()
+ }
+
+ private fun wrapGo(code: String, inputValues: List, @Suppress("UNUSED_PARAMETER") paramNames: List): String {
+ val args = inputValues.joinToString(", ") { toGoLiteral(it) }
+
+ val goFuncs = Regex("""func\s+(\w+)\s*\(""").findAll(code)
+ .map { it.groupValues[1] }
+ .filter { it != "main" }.toList()
+ val methodName = goFuncs.find { it == "solution" } ?: goFuncs.lastOrNull() ?: "solution"
+
+ // 사용자 코드에 package 선언이 있으면 제거 (래퍼가 package main을 제공)
+ val body = code.lines().filterNot { it.trim().startsWith("package ") }.joinToString("\n")
+
+ // fmt 대신 json/os만 import: 사용자 코드의 import "fmt"와 충돌 방지
+ return """
+package main
+
+import (
+ "encoding/json"
+ "os"
+)
+
+$body
+
+func main() {
+ _result := $methodName($args)
+ _b, _ := json.Marshal(_result)
+ os.Stdout.Write(append(_b, '\n'))
+}
+""".trimIndent()
+ }
+
+ private fun wrapRuby(code: String, inputValues: List, @Suppress("UNUSED_PARAMETER") paramNames: List): String {
+ val args = inputValues.joinToString(", ")
+
+ val rbMethods = Regex("""def\s+(\w+)""").findAll(code)
+ .map { it.groupValues[1] }
+ .filter { it != "initialize" }.toList()
+ val methodName = rbMethods.find { it == "solution" } ?: rbMethods.lastOrNull() ?: "solution"
+
+ return """
+require 'json'
+
+$code
+
+# 사용자 puts를 stderr로 리다이렉트
+_orig_stdout = ${'$'}stdout
+${'$'}stdout = ${'$'}stderr
+_result = $methodName($args)
+# stdout 복원 후 리턴값만 출력
+${'$'}stdout = _orig_stdout
+if _result.is_a?(String)
+ puts "\"#{_result}\""
+elsif _result.is_a?(Array)
+ puts _result.to_json
+else
+ puts _result
+end
+""".trimIndent()
+ }
+
// ─── 도구 경로 자동 감지 ───
private val javaHome: String by lazy { detectJavaHome() }
@@ -456,6 +623,22 @@ else console.log(_result);
private val gppPath: String by lazy { findExecutable("g++", "/usr/bin/g++", "/usr/local/bin/g++", "/opt/homebrew/bin/g++") }
private val kotlincPath: String by lazy { detectKotlinc() }
private val nodePath: String by lazy { detectNode() }
+ private val rustcPath: String by lazy {
+ findExecutable("rustc",
+ "${System.getProperty("user.home")}/.cargo/bin/rustc",
+ "/usr/local/bin/rustc", "/opt/homebrew/bin/rustc")
+ }
+ private val goPath: String by lazy {
+ findExecutable("go",
+ "/usr/local/go/bin/go", "/opt/homebrew/bin/go", "/usr/local/bin/go",
+ "${System.getProperty("user.home")}/go/bin/go")
+ }
+ private val rubyPath: String by lazy {
+ findExecutable("ruby",
+ "${System.getProperty("user.home")}/.rbenv/shims/ruby",
+ "/opt/homebrew/opt/ruby/bin/ruby", "/usr/local/opt/ruby/bin/ruby",
+ "/usr/bin/ruby")
+ }
private fun detectJavaHome(): String {
// 1. JAVA_HOME 환경변수
@@ -604,7 +787,10 @@ else console.log(_result);
"python" to pythonPath,
"node" to nodePath,
"g++" to gppPath,
- "kotlinc" to kotlincPath
+ "kotlinc" to kotlincPath,
+ "rustc" to rustcPath,
+ "go" to goPath,
+ "ruby" to rubyPath
)
// ─── 언어별 컴파일 & 실행 ───
@@ -769,8 +955,10 @@ else console.log(_result);
sourceFile.writeText(code)
val jarFile = File(dir, "solution.jar")
+ // -J-Dfile.encoding: javac의 -encoding UTF-8과 동일한 목적 (이슈 #2).
+ // Windows에서 kotlinc가 구버전 JDK(<18)로 실행되면 MS949로 소스를 읽어 한글이 깨지는 것을 방지.
val compile = executeProcess(
- listOf(kotlincPath, sourceFile.absolutePath, "-include-runtime", "-d", jarFile.absolutePath),
+ listOf(kotlincPath, "-J-Dfile.encoding=UTF-8", sourceFile.absolutePath, "-include-runtime", "-d", jarFile.absolutePath),
dir, "", timeout * 2
)
if (compile.exitCode != 0) {
@@ -792,6 +980,63 @@ else console.log(_result);
return executeProcess(listOf(nodePath, sourceFile.absolutePath), dir, input, timeout)
}
+ private fun runRust(code: String, input: String, dir: File, timeout: Long): RunResult {
+ if (rustcPath.isBlank()) {
+ return RunResult(output = "", error = I18n.t(
+ "rustc를 찾을 수 없습니다.\nhttps://rustup.rs 에서 Rust를 설치하세요.",
+ "rustc not found.\nPlease install Rust via https://rustup.rs"
+ ), exitCode = -1)
+ }
+ val sourceFile = File(dir, "solution.rs")
+ // rustc/go build는 g++(MinGW)와 달리 Windows에서 .exe를 자동으로 붙이지 않음
+ val outputFile = File(dir, if (isWindows) "solution.exe" else "solution")
+ sourceFile.writeText(code, StandardCharsets.UTF_8)
+
+ val compile = executeProcess(
+ listOf(rustcPath, "-O", "--edition", "2021", "-o", outputFile.absolutePath, sourceFile.absolutePath),
+ dir, "", timeout * 2
+ )
+ if (compile.exitCode != 0) {
+ return RunResult(output = "", error = I18n.t("컴파일 에러", "Compile error") + ":\n${compile.error}", exitCode = compile.exitCode)
+ }
+
+ return executeProcess(listOf(outputFile.absolutePath), dir, input, timeout)
+ }
+
+ private fun runGo(code: String, input: String, dir: File, timeout: Long): RunResult {
+ if (goPath.isBlank()) {
+ return RunResult(output = "", error = I18n.t(
+ "Go를 찾을 수 없습니다.\nhttps://go.dev/dl 에서 Go를 설치하세요.",
+ "Go not found.\nPlease install Go via https://go.dev/dl"
+ ), exitCode = -1)
+ }
+ val sourceFile = File(dir, "solution.go")
+ val outputFile = File(dir, if (isWindows) "solution.exe" else "solution")
+ sourceFile.writeText(code, StandardCharsets.UTF_8)
+
+ val compile = executeProcess(
+ listOf(goPath, "build", "-o", outputFile.absolutePath, sourceFile.absolutePath),
+ dir, "", timeout * 2
+ )
+ if (compile.exitCode != 0) {
+ return RunResult(output = "", error = I18n.t("컴파일 에러", "Compile error") + ":\n${compile.error}", exitCode = compile.exitCode)
+ }
+
+ return executeProcess(listOf(outputFile.absolutePath), dir, input, timeout)
+ }
+
+ private fun runRuby(code: String, input: String, dir: File, timeout: Long): RunResult {
+ if (rubyPath.isBlank()) {
+ return RunResult(output = "", error = I18n.t(
+ "Ruby를 찾을 수 없습니다.\nbrew install ruby 또는 rbenv로 설치하세요.",
+ "Ruby not found.\nPlease install via: brew install ruby"
+ ), exitCode = -1)
+ }
+ val sourceFile = File(dir, "solution.rb")
+ sourceFile.writeText(code, StandardCharsets.UTF_8)
+ return executeProcess(listOf(rubyPath, sourceFile.absolutePath), dir, input, timeout)
+ }
+
private fun executeProcess(
command: List,
dir: File,
@@ -803,8 +1048,12 @@ else console.log(_result);
.redirectErrorStream(false)
.start()
+ // 입력이 없어도 stdin을 닫아 EOF를 전달해야 함.
+ // 닫지 않으면 stdin을 읽는 코드가 타임아웃까지 블로킹되어 허위 시간 초과가 발생하고 파이프 FD가 누수됨.
if (input.isNotBlank()) {
process.outputStream.bufferedWriter(Charsets.UTF_8).use { it.write(input) }
+ } else {
+ process.outputStream.close()
}
// 메모리 폴링 스레드 시작
@@ -851,8 +1100,8 @@ else console.log(_result);
return RunResult(output = "", error = I18n.t("시간 초과 (${timeout}초)", "Time Limit Exceeded (${timeout}s)"), exitCode = -1, timedOut = true, executionTimeMs = elapsedMs, peakMemoryKB = peakMemory.get())
}
- val output = process.inputStream.bufferedReader(Charsets.UTF_8).readText().trimEnd()
- val error = process.errorStream.bufferedReader(Charsets.UTF_8).readText().trimEnd()
+ val output = process.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }.trimEnd()
+ val error = process.errorStream.bufferedReader(Charsets.UTF_8).use { it.readText() }.trimEnd()
return RunResult(output = output, error = error, exitCode = process.exitValue(), executionTimeMs = elapsedMs, peakMemoryKB = peakMemory.get())
}
diff --git a/src/main/kotlin/com/codingtestkit/service/CodeforcesCrawler.kt b/src/main/kotlin/com/codingtestkit/service/CodeforcesCrawler.kt
index 63703ce..efc1c6f 100644
--- a/src/main/kotlin/com/codingtestkit/service/CodeforcesCrawler.kt
+++ b/src/main/kotlin/com/codingtestkit/service/CodeforcesCrawler.kt
@@ -21,14 +21,29 @@ object CodeforcesCrawler {
return match.groupValues[1] to match.groupValues[2].uppercase()
}
- fun fetchProblem(problemId: String): Problem {
+ fun fetchProblem(problemId: String, cookies: String = ""): Problem {
val (contestId, letter) = parseProblemId(problemId)
- val doc = Jsoup.connect("${BASE_URL}$contestId/$letter?locale=en")
- .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
+ // 로그인 쿠키(cf_clearance 포함)를 함께 보내면 Cloudflare 챌린지를 통과할 수 있음.
+ // cf_clearance는 User-Agent에 바인딩되므로 JCEF(Chrome)와 유사한 UA를 사용.
+ val connection = Jsoup.connect("${BASE_URL}$contestId/$letter?locale=en")
+ .userAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")
.timeout(30000)
- .get()
+ if (cookies.isNotBlank()) connection.header("Cookie", cookies)
+ val doc = connection.get()
+ return parseProblemDoc(doc, contestId, letter)
+ }
+
+ /**
+ * JCEF 폴백 경로: 브라우저가 렌더링한 HTML을 동일한 파서로 처리.
+ * Cloudflare가 Jsoup의 TLS 지문을 차단할 때 사용된다.
+ */
+ fun parseProblemHtml(html: String, problemId: String): Problem {
+ val (contestId, letter) = parseProblemId(problemId)
+ return parseProblemDoc(Jsoup.parse(html, "https://codeforces.com"), contestId, letter)
+ }
+ private fun parseProblemDoc(doc: org.jsoup.nodes.Document, contestId: String, letter: String): Problem {
// 이미지: 상대 경로 → 절대 경로, 고정 크기 제거
doc.select("img[src]").forEach { img ->
val src = img.attr("src")
diff --git a/src/main/kotlin/com/codingtestkit/service/CodeforcesJcefFetcher.kt b/src/main/kotlin/com/codingtestkit/service/CodeforcesJcefFetcher.kt
new file mode 100644
index 0000000..9aa72e0
--- /dev/null
+++ b/src/main/kotlin/com/codingtestkit/service/CodeforcesJcefFetcher.kt
@@ -0,0 +1,186 @@
+package com.codingtestkit.service
+
+import com.codingtestkit.model.Problem
+import com.intellij.openapi.diagnostic.Logger
+import com.intellij.ui.jcef.JBCefApp
+import com.intellij.ui.jcef.JBCefBrowser
+import com.intellij.ui.jcef.JBCefBrowserBase
+import com.intellij.ui.jcef.JBCefJSQuery
+import org.cef.browser.CefBrowser
+import org.cef.browser.CefFrame
+import org.cef.handler.CefLoadHandlerAdapter
+import java.awt.Dimension
+import java.awt.Window
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.swing.JFrame
+import javax.swing.SwingUtilities
+import javax.swing.Timer
+
+/**
+ * JCEF 오프스크린 브라우저를 사용한 Codeforces 문제 가져오기 폴백.
+ *
+ * Codeforces는 Cloudflare가 비브라우저 TLS 지문(Jsoup/Java)을 403으로 차단할 수 있다.
+ * 실제 Chromium(JCEF)으로 페이지를 열면 챌린지를 통과하므로, 렌더링된 HTML을
+ * 추출해 CodeforcesCrawler의 기존 파서에 넘긴다. (SweaJcefFetcher와 동일 패턴)
+ */
+object CodeforcesJcefFetcher {
+
+ private val LOG = Logger.getInstance(CodeforcesJcefFetcher::class.java)
+
+ var lastError: String = ""
+ private set
+
+ // Cloudflare 챌린지 통과 시간(수 초~수십 초)을 포함한 타임아웃
+ private const val FETCH_TIMEOUT_MS = 60_000L
+
+ fun isAvailable(): Boolean = try {
+ JBCefApp.isSupported()
+ } catch (_: Exception) {
+ false
+ }
+
+ /**
+ * 백그라운드 스레드에서 호출할 것 (EDT에서 호출하면 deadlock).
+ */
+ fun fetchProblem(problemId: String): Problem? {
+ if (!isAvailable()) {
+ lastError = "JCEF not available"
+ return null
+ }
+
+ val future = CompletableFuture()
+
+ SwingUtilities.invokeLater {
+ try {
+ FetchSession(problemId, future).start()
+ } catch (e: Exception) {
+ LOG.warn("[CodingTestKit] CodeforcesJcefFetcher init failed", e)
+ lastError = "Init: ${e.message}"
+ if (!future.isDone) future.complete(null)
+ }
+ }
+
+ return try {
+ future.get(FETCH_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ } catch (_: TimeoutException) {
+ lastError = "Timeout (${FETCH_TIMEOUT_MS / 1000}s)"
+ LOG.info("[CodingTestKit] Codeforces JCEF fetch timeout: $problemId")
+ null
+ } catch (e: Exception) {
+ lastError = "Error: ${e.message}"
+ LOG.warn("[CodingTestKit] Codeforces JCEF fetch error", e)
+ null
+ }
+ }
+
+ private class FetchSession(
+ private val problemId: String,
+ private val future: CompletableFuture
+ ) {
+ private val done = AtomicBoolean(false)
+ private var frame: JFrame? = null
+ private var browser: JBCefBrowser? = null
+ private var timeoutTimer: Timer? = null
+
+ fun start() {
+ val (contestId, letter) = CodeforcesCrawler.parseProblemId(problemId)
+ val url = "https://codeforces.com/problemset/problem/$contestId/$letter?locale=en"
+
+ frame = JFrame().apply {
+ type = Window.Type.UTILITY
+ isUndecorated = true
+ size = Dimension(1024, 768)
+ setLocation(-3000, -3000)
+ }
+
+ browser = JBCefBrowser(url)
+ frame!!.contentPane.add(browser!!.component)
+ frame!!.isVisible = true
+
+ LOG.info("[CodingTestKit] Codeforces JCEF fetch started: $problemId")
+
+ val jsQuery = JBCefJSQuery.create(browser as JBCefBrowserBase)
+ jsQuery.addHandler { html ->
+ handleHtml(html)
+ JBCefJSQuery.Response("")
+ }
+
+ timeoutTimer = Timer((FETCH_TIMEOUT_MS - 5000).toInt()) {
+ complete(null, "JCEF internal timeout")
+ }.apply { isRepeats = false; start() }
+
+ browser!!.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() {
+ override fun onLoadEnd(cefBrowser: CefBrowser?, cefFrame: CefFrame?, httpStatusCode: Int) {
+ if (cefFrame?.isMain != true || done.get()) return
+ LOG.info("[CodingTestKit] Codeforces JCEF loaded: ${cefBrowser?.url} (status=$httpStatusCode)")
+ // Cloudflare 챌린지 페이지일 수 있으므로 .problem-statement가
+ // 나타날 때까지 폴링. 챌린지 통과 후 리다이렉트되면 onLoadEnd가
+ // 다시 불리고 새 문서에 폴러가 재주입된다.
+ startPolling(cefBrowser, jsQuery)
+ }
+ }, browser!!.cefBrowser)
+ }
+
+ private fun startPolling(cefBrowser: CefBrowser?, jsQuery: JBCefJSQuery) {
+ if (cefBrowser == null || done.get()) return
+ val queryJs = jsQuery.inject("html")
+ val js = """
+ (function() {
+ if (window.__ctkCfPoller) return;
+ var attempts = 0;
+ window.__ctkCfPoller = setInterval(function() {
+ attempts++;
+ if (document.querySelector('.problem-statement')) {
+ clearInterval(window.__ctkCfPoller);
+ var html = document.documentElement.outerHTML;
+ $queryJs
+ } else if (attempts >= 90) {
+ clearInterval(window.__ctkCfPoller);
+ var html = '';
+ $queryJs
+ }
+ }, 500);
+ })();
+ """.trimIndent()
+ cefBrowser.executeJavaScript(js, cefBrowser.url, 0)
+ }
+
+ private fun handleHtml(html: String) {
+ if (html.isBlank() || !html.contains("problem-statement")) {
+ complete(null, "Cloudflare challenge not passed or unexpected page")
+ return
+ }
+ try {
+ val problem = CodeforcesCrawler.parseProblemHtml(html, problemId)
+ if (problem.title.isNotBlank() || problem.description.length > 50) {
+ complete(problem)
+ } else {
+ complete(null, "Parsed content empty")
+ }
+ } catch (e: Exception) {
+ complete(null, "Parse: ${e.message}")
+ }
+ }
+
+ private fun complete(problem: Problem?, error: String? = null) {
+ if (!done.compareAndSet(false, true)) return
+ if (error != null) {
+ lastError = error
+ LOG.info("[CodingTestKit] Codeforces JCEF fetch error: $error")
+ } else {
+ LOG.info("[CodingTestKit] Codeforces JCEF fetch OK: ${problem?.title}")
+ }
+ timeoutTimer?.stop()
+
+ SwingUtilities.invokeLater {
+ try { browser?.dispose() } catch (_: Exception) {}
+ try { frame?.dispose() } catch (_: Exception) {}
+ }
+
+ if (!future.isDone) future.complete(problem)
+ }
+ }
+}
diff --git a/src/main/kotlin/com/codingtestkit/service/DuplicateClassHighlightFilter.kt b/src/main/kotlin/com/codingtestkit/service/DuplicateClassHighlightFilter.kt
index e9a3470..3170892 100644
--- a/src/main/kotlin/com/codingtestkit/service/DuplicateClassHighlightFilter.kt
+++ b/src/main/kotlin/com/codingtestkit/service/DuplicateClassHighlightFilter.kt
@@ -17,14 +17,12 @@ class DuplicateClassHighlightFilter : HighlightInfoFilter {
if (file == null) return true
if (info.severity != HighlightSeverity.ERROR) return true
val desc = info.description ?: return true
- val filePath = file.virtualFile?.path ?: return true
+ if (file.virtualFile == null) return true
- // src 파일에서 problems/ 내 파일을 가리키는 중복 클래스 오류 숨김
+ // 중복 클래스 오류 메시지가 problems/ 내 파일을 가리키면 숨김
// 예: "파일 '/path/problems/.../Main.java'에서 중복된 클래스가 발견되었습니다"
- if (desc.contains("/problems/")) return false
-
- // problems 파일에서 외부 파일을 가리키는 중복 클래스 오류 숨김
- if (filePath.contains("/problems/") && desc.contains("\\problems\\")) return false
+ // Windows는 메시지의 경로가 \ 구분자로 나오므로 통일 후 비교
+ if (desc.replace('\\', '/').contains("/problems/")) return false
return true
}
diff --git a/src/main/kotlin/com/codingtestkit/service/GitHubService.kt b/src/main/kotlin/com/codingtestkit/service/GitHubService.kt
index 68f7dc8..ad24e72 100644
--- a/src/main/kotlin/com/codingtestkit/service/GitHubService.kt
+++ b/src/main/kotlin/com/codingtestkit/service/GitHubService.kt
@@ -45,12 +45,33 @@ class GitHubService : PersistentStateComponent {
private val tokenCredentialAttributes: CredentialAttributes =
CredentialAttributes(generateServiceName("CodingTestKit", "github.token"))
+ /**
+ * PasswordSafe(OS 키체인) 접근은 느려서 EDT에서 호출하면 SlowOperations 위반.
+ * AuthService와 동일하게 메모리 캐시로 읽고, 쓰기는 백그라운드로 영속화한다.
+ */
+ @Volatile
+ private var cachedToken: String? = null
+ private val persistExecutor = java.util.concurrent.Executors.newSingleThreadExecutor { r ->
+ Thread(r, "CodingTestKit-GitHubPersist").apply { isDaemon = true }
+ }
+
+ init {
+ persistExecutor.execute {
+ if (cachedToken == null) {
+ cachedToken = PasswordSafe.instance.getPassword(tokenCredentialAttributes) ?: ""
+ }
+ }
+ }
+
private fun writeToken(value: String) {
- PasswordSafe.instance.setPassword(tokenCredentialAttributes, value.ifBlank { null })
+ cachedToken = value
+ persistExecutor.execute {
+ PasswordSafe.instance.setPassword(tokenCredentialAttributes, value.ifBlank { null })
+ }
}
private fun readToken(): String =
- PasswordSafe.instance.getPassword(tokenCredentialAttributes) ?: ""
+ cachedToken ?: (PasswordSafe.instance.getPassword(tokenCredentialAttributes) ?: "").also { cachedToken = it }
val token: String get() = readToken()
val repoFullName: String get() = state.repoFullName
diff --git a/src/main/kotlin/com/codingtestkit/service/LeetCodeApi.kt b/src/main/kotlin/com/codingtestkit/service/LeetCodeApi.kt
index 6d43985..4acd4f0 100644
--- a/src/main/kotlin/com/codingtestkit/service/LeetCodeApi.kt
+++ b/src/main/kotlin/com/codingtestkit/service/LeetCodeApi.kt
@@ -309,6 +309,9 @@ object LeetCodeApi {
"c++", "cpp" -> "cpp"
"kotlin", "kt" -> "kotlin"
"javascript", "js" -> "javascript"
+ "rust", "rs" -> "rust"
+ "go", "golang" -> "golang"
+ "ruby", "rb" -> "ruby"
else -> "java"
}
diff --git a/src/main/kotlin/com/codingtestkit/service/ProblemFileManager.kt b/src/main/kotlin/com/codingtestkit/service/ProblemFileManager.kt
index 1101fcf..4dedde3 100644
--- a/src/main/kotlin/com/codingtestkit/service/ProblemFileManager.kt
+++ b/src/main/kotlin/com/codingtestkit/service/ProblemFileManager.kt
@@ -291,7 +291,7 @@ object ProblemFileManager {
}
if (matchingDir != null) {
val codeFile = matchingDir.listFiles()?.firstOrNull {
- it.extension in listOf("java", "py", "cpp", "kt", "js")
+ it.extension in Language.entries.map { lang -> lang.extension }
}
if (codeFile != null) {
return LocalFileSystem.getInstance().refreshAndFindFileByIoFile(codeFile)
diff --git a/src/main/kotlin/com/codingtestkit/service/SubmitService.kt b/src/main/kotlin/com/codingtestkit/service/SubmitService.kt
index d930bbe..058eb40 100644
--- a/src/main/kotlin/com/codingtestkit/service/SubmitService.kt
+++ b/src/main/kotlin/com/codingtestkit/service/SubmitService.kt
@@ -39,7 +39,10 @@ object SubmitService {
Language.PYTHON to "python3",
Language.CPP to "cpp",
Language.KOTLIN to "kotlin",
- Language.JAVASCRIPT to "javascript"
+ Language.JAVASCRIPT to "javascript",
+ // Rust는 프로그래머스 미지원 → 맵에서 제외 (미지원 안내 처리)
+ Language.GO to "go",
+ Language.RUBY to "ruby"
)
private fun submitProgrammers(
diff --git a/src/main/kotlin/com/codingtestkit/ui/CodeSubmitDialog.kt b/src/main/kotlin/com/codingtestkit/ui/CodeSubmitDialog.kt
index ad239f5..03044d5 100644
--- a/src/main/kotlin/com/codingtestkit/ui/CodeSubmitDialog.kt
+++ b/src/main/kotlin/com/codingtestkit/ui/CodeSubmitDialog.kt
@@ -163,7 +163,9 @@ class CodeSubmitDialog(
Language.PYTHON to "python3",
Language.CPP to "cpp",
Language.KOTLIN to "kotlin",
- Language.JAVASCRIPT to "javascript"
+ Language.JAVASCRIPT to "javascript",
+ Language.GO to "go",
+ Language.RUBY to "ruby"
)
private fun handleProgrammersLoad(cefBrowser: CefBrowser?, url: String) {
@@ -190,12 +192,16 @@ class CodeSubmitDialog(
Language.CPP -> "C++"
Language.KOTLIN -> "Kotlin"
Language.JAVASCRIPT -> "JavaScript"
+ Language.RUST -> "Rust" // 프로그래머스 미지원 — 드롭다운에 없으므로 언어 전환 없이 코드만 입력됨
+ Language.GO -> "Go"
+ Language.RUBY -> "Ruby"
}
private fun injectProgrammersCode(cefBrowser: CefBrowser?) {
if (cefBrowser == null) return
val escaped = escapeForJs(code)
- val langValue = programmersLangMap[language] ?: "java"
+ // 맵에 없는 언어(프로그래머스 미지원)는 드롭다운과 매칭되지 않는 값으로 두어 언어 전환을 건너뜀
+ val langValue = programmersLangMap[language] ?: language.displayName.lowercase()
val langDisplay = getProgrammersLangDisplayName()
val js = """
@@ -420,6 +426,9 @@ class CodeSubmitDialog(
Language.CPP -> "C++"
Language.KOTLIN -> "Kotlin"
Language.JAVASCRIPT -> "JavaScript"
+ Language.RUST -> "Rust"
+ Language.GO -> "Go"
+ Language.RUBY -> "Ruby"
}
private fun injectLeetCodeCode(cefBrowser: CefBrowser?) {
@@ -514,6 +523,9 @@ class CodeSubmitDialog(
Language.CPP -> "GNU C++20"
Language.KOTLIN -> "Kotlin"
Language.JAVASCRIPT -> "Node.js"
+ Language.RUST -> "Rust"
+ Language.GO -> "Go"
+ Language.RUBY -> "Ruby"
}
private fun handleCodeforcesLoad(cefBrowser: CefBrowser?, url: String) {
diff --git a/src/main/kotlin/com/codingtestkit/ui/MainPanel.kt b/src/main/kotlin/com/codingtestkit/ui/MainPanel.kt
index 7719dbf..304236a 100644
--- a/src/main/kotlin/com/codingtestkit/ui/MainPanel.kt
+++ b/src/main/kotlin/com/codingtestkit/ui/MainPanel.kt
@@ -40,6 +40,11 @@ class MainPanel(private val project: Project) : JPanel(BorderLayout()), Disposab
add(tabbedPane, BorderLayout.CENTER)
+ // Problems ↔ Tests 언어 선택 양방향 동기화
+ // (setLanguage가 같은 값이면 무시하므로 콜백이 무한 순환하지 않음)
+ problemPanel.onLanguageChanged = { testPanel.setLanguage(it) }
+ testPanel.onLanguageChanged = { problemPanel.setLanguage(it) }
+
// 문제를 가져오면 테스트 패널에 테스트 케이스 전달
problemPanel.onProblemFetched = { problem ->
testPanel.setProblemSource(problem.source)
diff --git a/src/main/kotlin/com/codingtestkit/ui/ProblemPanel.kt b/src/main/kotlin/com/codingtestkit/ui/ProblemPanel.kt
index 0d65473..2b0c47a 100644
--- a/src/main/kotlin/com/codingtestkit/ui/ProblemPanel.kt
+++ b/src/main/kotlin/com/codingtestkit/ui/ProblemPanel.kt
@@ -50,6 +50,11 @@ class ProblemPanel(private val project: Project) : JPanel(BorderLayout()) {
preferredSize = Dimension(JBUI.scale(90), preferredSize.height)
renderer = createComboRenderer()
}
+ private val submitSupportLabel = JLabel().apply {
+ font = font.deriveFont(JBUI.scaleFontSize(11f).toFloat())
+ foreground = JBColor(Color(0xB0, 0x6A, 0x00), Color(0xE0, 0xA0, 0x50))
+ isVisible = false
+ }
private val problemIdField = JTextField().apply {
toolTipText = I18n.t("문제 번호 또는 URL을 입력하세요", "Enter problem number or URL")
}
@@ -95,6 +100,9 @@ class ProblemPanel(private val project: Project) : JPanel(BorderLayout()) {
private var currentProblemHtml = ""
var onProblemFetched: ((Problem) -> Unit)? = null
+
+ /** 언어 선택 변경 알림 (Tests 탭과 양방향 동기화용) */
+ var onLanguageChanged: ((Language) -> Unit)? = null
private var currentProblem: Problem? = null
private var currentProblemFolder: java.io.File? = null
private var isTranslated = false
@@ -119,6 +127,7 @@ class ProblemPanel(private val project: Project) : JPanel(BorderLayout()) {
row1.add(languageCombo)
row1.add(loginButton)
row1.add(githubLoginButton)
+ row1.add(submitSupportLabel)
topPanel.add(row1)
// Row 2: 문제번호 입력 + 가져오기 (WrapLayout으로 반응형)
@@ -236,12 +245,19 @@ class ProblemPanel(private val project: Project) : JPanel(BorderLayout()) {
submitButton.isEnabled = false
githubPushButton.isEnabled = false
- updateLoginButton()
- updateGitHubButton()
- // 백그라운드에서 저장된 쿠키 유효성 검증
+ // 첫 서비스 접근은 PasswordSafe(키체인)를 읽으므로 EDT 금지 —
+ // 백그라운드에서 캐시를 예열한 뒤 버튼 상태를 갱신하고, 이어서 쿠키 유효성 검증
ApplicationManager.getApplication().executeOnPooledThread {
val auth = AuthService.getInstance()
+ val github = GitHubService.getInstance()
+ for (source in ProblemSource.entries) auth.getCookies(source)
+ github.token
+ SwingUtilities.invokeLater {
+ updateLoginButton()
+ updateGitHubButton()
+ }
+
for (source in ProblemSource.entries) {
if (auth.isLoggedIn(source)) {
if (!auth.validateSession(source)) {
@@ -255,6 +271,11 @@ class ProblemPanel(private val project: Project) : JPanel(BorderLayout()) {
sourceCombo.addActionListener {
updateLoginButton()
updatePlaceholder()
+ updateSubmitSupportHint()
+ }
+ languageCombo.addActionListener {
+ updateSubmitSupportHint()
+ onLanguageChanged?.invoke(getSelectedLanguage())
}
fetchButton.addActionListener { fetchProblem() }
randomButton.addActionListener { openRandomDialog() }
@@ -267,6 +288,7 @@ class ProblemPanel(private val project: Project) : JPanel(BorderLayout()) {
githubLoginButton.addActionListener { handleGitHubLogin() }
problemIdField.addActionListener { fetchProblem() }
updatePlaceholder()
+ updateSubmitSupportHint()
// 키보드 단축키 액션 등록
CodingTestKitActionService.getInstance(project).apply {
@@ -276,6 +298,23 @@ class ProblemPanel(private val project: Project) : JPanel(BorderLayout()) {
}
}
+ /**
+ * 선택한 (플랫폼, 언어) 조합이 제출 미지원이면 콤보 옆에 즉시 경고 표시.
+ * 문제를 다 푼 뒤 제출 시점에야 미지원을 알게 되는 상황을 방지한다.
+ */
+ private fun updateSubmitSupportHint() {
+ val source = getSelectedSource()
+ val language = getSelectedLanguage()
+ val submittable = language.isSubmittable(source)
+ submitSupportLabel.isVisible = !submittable
+ if (!submittable) {
+ submitSupportLabel.text = I18n.t(
+ "⚠ ${language.displayName} — ${source.localizedName()} 제출 미지원 (로컬 테스트만 가능)",
+ "⚠ ${language.displayName} — cannot submit to ${source.localizedName()} (local testing only)"
+ )
+ }
+ }
+
private fun updatePlaceholder() {
val source = getSelectedSource()
val (placeholder, tooltip) = when (source) {
@@ -334,6 +373,14 @@ class ProblemPanel(private val project: Project) : JPanel(BorderLayout()) {
private fun getSelectedSource(): ProblemSource = ProblemSource.entries[sourceCombo.selectedIndex]
private fun getSelectedLanguage(): Language = Language.entries[languageCombo.selectedIndex]
+ /** 외부(Tests 탭)에서 언어 동기화. 같은 값이면 무시해 콜백 루프를 끊는다. */
+ fun setLanguage(language: Language) {
+ val idx = Language.entries.indexOf(language)
+ if (idx >= 0 && languageCombo.selectedIndex != idx) {
+ languageCombo.selectedIndex = idx
+ }
+ }
+
private fun updateLoginButton() {
val source = getSelectedSource()
val auth = AuthService.getInstance()
@@ -659,7 +706,15 @@ class ProblemPanel(private val project: Project) : JPanel(BorderLayout()) {
val problem = when (source) {
ProblemSource.PROGRAMMERS -> ProgrammersCrawler.fetchProblem(id, cookies)
ProblemSource.LEETCODE -> LeetCodeApi.fetchProblem(id, language.extension, cookies)
- ProblemSource.CODEFORCES -> CodeforcesCrawler.fetchProblem(id)
+ ProblemSource.CODEFORCES -> try {
+ CodeforcesCrawler.fetchProblem(id, cookies)
+ } catch (e: Exception) {
+ // Cloudflare가 Jsoup을 차단(403)하면 실브라우저(JCEF)로 폴백
+ if (CodeforcesJcefFetcher.isAvailable()) {
+ CodeforcesJcefFetcher.fetchProblem(id)
+ ?: throw IllegalStateException("${e.message}\nJCEF fallback: ${CodeforcesJcefFetcher.lastError}")
+ } else throw e
+ }
ProblemSource.SWEA -> throw IllegalStateException("unreachable")
}
diff --git a/src/main/kotlin/com/codingtestkit/ui/TestPanel.kt b/src/main/kotlin/com/codingtestkit/ui/TestPanel.kt
index 13cb4c5..12e35bc 100644
--- a/src/main/kotlin/com/codingtestkit/ui/TestPanel.kt
+++ b/src/main/kotlin/com/codingtestkit/ui/TestPanel.kt
@@ -48,6 +48,17 @@ class TestPanel(private val project: Project) : JPanel(BorderLayout()) {
private var problemSource = ProblemSource.PROGRAMMERS
private var parameterNames = listOf()
+ /** 언어 선택 변경 알림 (Problems 탭과 양방향 동기화용) */
+ var onLanguageChanged: ((Language) -> Unit)? = null
+
+ /** 외부(Problems 탭)에서 언어 동기화. 같은 값이면 무시해 콜백 루프를 끊는다. */
+ fun setLanguage(language: Language) {
+ val idx = Language.entries.indexOf(language)
+ if (idx >= 0 && languageCombo.selectedIndex != idx) {
+ languageCombo.selectedIndex = idx
+ }
+ }
+
init {
border = JBUI.Borders.empty()
@@ -65,6 +76,7 @@ class TestPanel(private val project: Project) : JPanel(BorderLayout()) {
foreground = JBColor.GRAY
})
buttonRow.add(languageCombo)
+ languageCombo.addActionListener { onLanguageChanged?.invoke(getSelectedLanguage()) }
buttonRow.add(runButton)
buttonRow.add(addButton)
topPanel.add(buttonRow)
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 38a0a54..f437362 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -1,7 +1,7 @@
com.codingtestkit
CodingTestKit
- 1.4.4
+ 1.5.0
Su Beom
Supported Languages
- Java, Python, C++, Kotlin, JavaScript
+ Java, Python, C++, Kotlin, JavaScript, Rust, Go, Ruby
+ Local test execution works for all languages on every platform. Submission availability depends on each platform — unsupported combinations show a warning at selection time.
Getting Started
@@ -96,7 +97,7 @@
- 문제 가져오기 – 번호 입력만으로 문제 설명, 테스트 케이스 자동 추출
- 문제 보기 & 번역 – IDE 내에서 문제 확인 + KaTeX 수식 렌더링 + 한/영 원클릭 번역
- - 로컬 테스트 – Java, Python, C++, Kotlin, JavaScript 코드를 IDE에서 바로 실행 + 실행 시간 & 메모리
+ - 로컬 테스트 – Java, Python, C++, Kotlin, JavaScript, Rust, Go, Ruby 코드를 IDE에서 바로 실행 + 실행 시간 & 메모리
- 로그인 & 제출 – 내장 브라우저로 직접 제출 + 언어/코드/문제 자동 선택
- 문제 검색 – LeetCode, Codeforces, 프로그래머스에서 키워드·태그·난이도로 검색
- 랜덤 뽑기 – 티어/클래스/레이팅 범위, 태그 칩, 풀이 필터, 정답자 수 필터, 기출문제 모음 필터
@@ -109,7 +110,8 @@
지원 언어
- Java, Python, C++, Kotlin, JavaScript
+ Java, Python, C++, Kotlin, JavaScript, Rust, Go, Ruby
+ 로컬 테스트 실행은 모든 플랫폼에서 전 언어 지원. 제출은 플랫폼별로 다르며, 미지원 조합 선택 시 즉시 경고가 표시됩니다.
시작하기
@@ -155,6 +157,33 @@
]]>
v1.5.0
+ Rust, Go, and Ruby language support (requested in #7) + Cloudflare-resistant Codeforces fetching.
+
+ English
+
+ - [New] Rust / Go / Ruby support – Local test execution (compile/run + automatic toolchain detection incl.
~/.cargo/bin, /usr/local/go/bin, rbenv), Programmers-style solution wrappers with argument conversion, per-platform default templates, and submission for LeetCode / Codeforces (Programmers supports Go/Ruby; Rust is not supported by that platform).
+ - [New] Submit-support early warning – Selecting a (platform, language) combination that can't be submitted (e.g. Rust × Programmers, Kotlin × SWEA) shows an immediate hint next to the selectors instead of failing at submission time. Local testing works for every combination.
+ - [New] Codeforces Cloudflare fallback – When Cloudflare blocks HTTP crawling (403), problem fetching automatically falls back to an offscreen embedded browser (JCEF) that passes the challenge. Fetch may take a few extra seconds when the fallback engages.
+ - [New] Language sync between tabs – The language selector in the Problems tab and Tests tab now stay in sync bidirectionally.
+ - [Fix] False "Time Limit Exceeded" on empty input – stdin is now always closed so programs reading input see EOF instead of blocking until timeout; also fixes a pipe file-descriptor leak on every compile.
+ - [Fix] Kotlin compilation encoding on Windows –
kotlinc now runs with -J-Dfile.encoding=UTF-8 (same fix as javac in v1.4.3), preventing garbled Korean source with older JDKs.
+ - [Fix] Duplicate-class error suppression on Windows – The highlight filter now handles Windows path separators.
+ - [Fix] UI freezes from keychain access – Login cookies and the GitHub token are no longer read from the OS keychain on the UI thread (SlowOperations violations); they are served from in-memory caches with background persistence.
+
+
+ 한국어
+
+ - [신규] Rust / Go / Ruby 지원 – 로컬 테스트 실행(컴파일/실행 +
~/.cargo/bin, /usr/local/go/bin, rbenv 등 툴체인 자동 탐지), 프로그래머스식 solution 래퍼(인자 자동 변환), 플랫폼별 기본 템플릿, LeetCode/Codeforces 제출 지원 (프로그래머스는 Go/Ruby 지원, Rust는 플랫폼 미지원). 이슈 #7 요청 반영.
+ - [신규] 제출 미지원 조합 즉시 경고 – 제출이 불가능한 (플랫폼, 언어) 조합(예: Rust × 프로그래머스, Kotlin × SWEA)을 선택하면 제출 시점이 아니라 선택 즉시 콤보 옆에 안내가 표시됩니다. 로컬 테스트는 모든 조합에서 가능.
+ - [신규] Codeforces Cloudflare 폴백 – HTTP 크롤링이 Cloudflare에 차단(403)되면 오프스크린 내장 브라우저(JCEF)가 챌린지를 통과해 문제를 가져옵니다. 폴백 동작 시 몇 초 더 소요될 수 있습니다.
+ - [신규] 탭 간 언어 동기화 – 문제 탭과 테스트 탭의 언어 선택이 양방향으로 자동 동기화됩니다.
+ - [수정] 빈 입력에서 허위 "시간 초과" – stdin을 항상 닫아 입력을 읽는 프로그램이 타임아웃까지 블로킹되지 않고 EOF를 받도록 수정. 컴파일마다 발생하던 파이프 FD 누수도 함께 해결.
+ - [수정] Windows Kotlin 컴파일 인코딩 –
kotlinc에 -J-Dfile.encoding=UTF-8 적용 (v1.4.3의 javac 수정과 동일 목적). 구버전 JDK에서 한글 소스가 깨지는 것을 방지.
+ - [수정] Windows 중복 클래스 오류 억제 – 하이라이트 필터가 Windows 경로 구분자(\)를 처리하도록 수정.
+ - [수정] 키체인 접근으로 인한 UI 멈칫거림 – 로그인 쿠키와 GitHub 토큰을 UI 스레드에서 OS 키체인으로 읽지 않도록(SlowOperations 위반) 메모리 캐시 + 백그라운드 영속화로 변경.
+
+
v1.4.4
IntelliJ 2026.2 compatibility. The problem viewer now works across IDE versions with different JCEF APIs.
diff --git a/src/test/kotlin/com/codingtestkit/model/ModelsTest.kt b/src/test/kotlin/com/codingtestkit/model/ModelsTest.kt
index cec6da7..a1411be 100644
--- a/src/test/kotlin/com/codingtestkit/model/ModelsTest.kt
+++ b/src/test/kotlin/com/codingtestkit/model/ModelsTest.kt
@@ -47,6 +47,41 @@ class ModelsTest {
assertEquals("py", Language.PYTHON.extension)
assertEquals("cpp", Language.CPP.extension)
assertEquals("kt", Language.KOTLIN.extension)
+ assertEquals("js", Language.JAVASCRIPT.extension)
+ assertEquals("rs", Language.RUST.extension)
+ assertEquals("go", Language.GO.extension)
+ assertEquals("rb", Language.RUBY.extension)
+ }
+
+ @Test
+ fun `Rust Go Ruby defaultCode for Programmers has solution function`() {
+ assertTrue(Language.RUST.defaultCode(ProblemSource.PROGRAMMERS).contains("fn solution"))
+ assertTrue(Language.GO.defaultCode(ProblemSource.PROGRAMMERS).contains("func solution"))
+ assertTrue(Language.RUBY.defaultCode(ProblemSource.PROGRAMMERS).contains("def solution"))
+ }
+
+ @Test
+ fun `Rust Go Ruby are not submittable to SWEA`() {
+ assertTrue(Language.RUST.sweaId < 0)
+ assertTrue(Language.GO.sweaId < 0)
+ assertTrue(Language.RUBY.sweaId < 0)
+ }
+
+ @Test
+ fun `isSubmittable matrix matches platform support`() {
+ // 리트코드/코드포스는 전 언어 제출 가능
+ for (lang in Language.entries) {
+ assertTrue(lang.isSubmittable(ProblemSource.LEETCODE))
+ assertTrue(lang.isSubmittable(ProblemSource.CODEFORCES))
+ }
+ // 프로그래머스는 Rust만 미지원
+ assertFalse(Language.RUST.isSubmittable(ProblemSource.PROGRAMMERS))
+ assertTrue(Language.GO.isSubmittable(ProblemSource.PROGRAMMERS))
+ assertTrue(Language.RUBY.isSubmittable(ProblemSource.PROGRAMMERS))
+ // SWEA는 sweaId 기반
+ assertTrue(Language.JAVA.isSubmittable(ProblemSource.SWEA))
+ assertFalse(Language.KOTLIN.isSubmittable(ProblemSource.SWEA))
+ assertFalse(Language.RUST.isSubmittable(ProblemSource.SWEA))
}
@Test
diff --git a/src/test/kotlin/com/codingtestkit/service/CodeRunnerTest.kt b/src/test/kotlin/com/codingtestkit/service/CodeRunnerTest.kt
index 28b9742..c9cdb96 100644
--- a/src/test/kotlin/com/codingtestkit/service/CodeRunnerTest.kt
+++ b/src/test/kotlin/com/codingtestkit/service/CodeRunnerTest.kt
@@ -426,4 +426,169 @@ class CodeRunnerTest {
// The caller's arguments must be preserved at the end of the command.
assertEquals(listOf("-cp", "X", "Main"), cmd.takeLast(3))
}
+
+ // ── Rust / Go / Ruby (이슈 #7) ──
+
+ private fun isToolAvailable(vararg command: String): Boolean {
+ return try {
+ ProcessBuilder(*command).start().waitFor() == 0
+ } catch (_: Exception) { false }
+ }
+
+ @Test
+ fun `hasMainFunction detects Rust Go Ruby main`() {
+ val method = CodeRunner::class.java.getDeclaredMethod(
+ "hasMainFunction", String::class.java, Language::class.java
+ )
+ method.isAccessible = true
+
+ assertTrue(method.invoke(CodeRunner, "fn main() { }", Language.RUST) as Boolean)
+ assertFalse(method.invoke(CodeRunner, "fn solution() -> i32 { 0 }", Language.RUST) as Boolean)
+ assertTrue(method.invoke(CodeRunner, "func main() { }", Language.GO) as Boolean)
+ assertFalse(method.invoke(CodeRunner, "func solution() int { return 0 }", Language.GO) as Boolean)
+ // Ruby는 stdin 사용 여부로 스크립트 스타일 판별
+ assertTrue(method.invoke(CodeRunner, "n = gets.to_i", Language.RUBY) as Boolean)
+ assertFalse(method.invoke(CodeRunner, "def solution()\n 0\nend", Language.RUBY) as Boolean)
+ }
+
+ @Test
+ fun `run Rust hello world`() {
+ if (!isToolAvailable("rustc", "--version")) return
+
+ val code = """
+ use std::io::{self, Read};
+
+ fn main() {
+ let mut input = String::new();
+ io::stdin().read_to_string(&mut input).unwrap();
+ let mut it = input.split_whitespace();
+ let a: i64 = it.next().unwrap().parse().unwrap();
+ let b: i64 = it.next().unwrap().parse().unwrap();
+ println!("{}", a + b);
+ }
+ """.trimIndent()
+
+ val tc = TestCase(input = "1 2", expectedOutput = "3")
+ val result = CodeRunner.run(code, Language.RUST, tc)
+
+ assertEquals(0, result.exitCode, "Exit code should be 0: ${result.error}")
+ assertEquals("3", result.output.trim())
+ }
+
+ @Test
+ fun `run Go hello world`() {
+ if (!isToolAvailable("go", "version")) return
+
+ val code = """
+ package main
+
+ import "fmt"
+
+ func main() {
+ var a, b int
+ fmt.Scan(&a, &b)
+ fmt.Println(a + b)
+ }
+ """.trimIndent()
+
+ val tc = TestCase(input = "3 4", expectedOutput = "7")
+ val result = CodeRunner.run(code, Language.GO, tc)
+
+ assertEquals(0, result.exitCode, "Exit code should be 0: ${result.error}")
+ assertEquals("7", result.output.trim())
+ }
+
+ @Test
+ fun `run Ruby hello world`() {
+ if (!isToolAvailable("ruby", "--version")) return
+
+ val code = """
+ a, b = gets.split.map(&:to_i)
+ puts a + b
+ """.trimIndent()
+
+ val tc = TestCase(input = "5 6", expectedOutput = "11")
+ val result = CodeRunner.run(code, Language.RUBY, tc)
+
+ assertEquals(0, result.exitCode, "Exit code should be 0: ${result.error}")
+ assertEquals("11", result.output.trim())
+ }
+
+ @Test
+ fun `run Rust programmers wrapper with vec argument`() {
+ if (!isToolAvailable("rustc", "--version")) return
+
+ val code = """
+ fn solution(arr: Vec) -> i32 {
+ arr.iter().sum()
+ }
+ """.trimIndent()
+
+ val tc = TestCase(input = "[1,2,3]", expectedOutput = "6")
+ val result = CodeRunner.runProgrammers(code, Language.RUST, tc, listOf("arr"))
+
+ assertEquals(0, result.exitCode, "Exit code should be 0: ${result.error}")
+ assertEquals("6", result.output.trim())
+ }
+
+ @Test
+ fun `run Go programmers wrapper returns compact array`() {
+ if (!isToolAvailable("go", "version")) return
+
+ val code = """
+ func solution(arr []int) []int {
+ doubled := make([]int, len(arr))
+ for i, v := range arr {
+ doubled[i] = v * 2
+ }
+ return doubled
+ }
+ """.trimIndent()
+
+ val tc = TestCase(input = "[1,2,3]", expectedOutput = "[2,4,6]")
+ val result = CodeRunner.runProgrammers(code, Language.GO, tc, listOf("arr"))
+
+ assertEquals(0, result.exitCode, "Exit code should be 0: ${result.error}")
+ assertEquals("[2,4,6]", result.output.trim())
+ }
+
+ @Test
+ fun `run Ruby programmers wrapper redirects user output`() {
+ if (!isToolAvailable("ruby", "--version")) return
+
+ val code = """
+ def solution(arr)
+ puts "debug output"
+ arr.map { |x| x * 2 }
+ end
+ """.trimIndent()
+
+ val tc = TestCase(input = "[1, 2, 3]", expectedOutput = "[2,4,6]")
+ val result = CodeRunner.runProgrammers(code, Language.RUBY, tc, listOf("arr"))
+
+ assertEquals(0, result.exitCode, "Exit code should be 0: ${result.error}")
+ // 사용자 puts는 stderr로 리다이렉트되고 stdout에는 리턴값만 출력
+ assertEquals("[2,4,6]", result.output.trim())
+ }
+
+ /**
+ * 빈 입력이어도 자식 프로세스의 stdin이 닫혀 EOF가 전달되어야 한다.
+ * 닫지 않으면 stdin을 읽는 코드가 타임아웃까지 블로킹되어 허위 시간 초과가 발생한다.
+ */
+ @Test
+ fun `run with blank input closes stdin so child sees EOF`() {
+ if (!isPythonAvailable()) return
+
+ val code = """
+ import sys
+ print(len(sys.stdin.read()))
+ """.trimIndent()
+
+ val tc = TestCase(input = "", expectedOutput = "0")
+ val result = CodeRunner.run(code, Language.PYTHON, tc, timeoutSeconds = 3)
+
+ assertFalse(result.timedOut, "Should not time out on blank input: ${result.error}")
+ assertEquals(0, result.exitCode, "Exit code should be 0: ${result.error}")
+ assertEquals("0", result.output.trim())
+ }
}
diff --git a/src/test/kotlin/com/codingtestkit/service/LeetCodeStyleSmokeTest.kt b/src/test/kotlin/com/codingtestkit/service/LeetCodeStyleSmokeTest.kt
new file mode 100644
index 0000000..4862242
--- /dev/null
+++ b/src/test/kotlin/com/codingtestkit/service/LeetCodeStyleSmokeTest.kt
@@ -0,0 +1,120 @@
+package com.codingtestkit.service
+
+import com.codingtestkit.model.Language
+import com.codingtestkit.model.TestCase
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+
+/**
+ * 리트코드가 실제로 주는 초기 코드 형태(two_sum/twoSum 네이밍, impl Solution 등)가
+ * 프로그래머스식 래퍼(runProgrammers)를 올바르게 통과하는지 검증.
+ * 입력은 실제 exampleTestcases 포맷(파라미터별 한 줄)을 사용한다.
+ * 해당 툴체인이 없는 환경에서는 조용히 스킵된다.
+ */
+class LeetCodeStyleSmokeTest {
+
+ private fun available(vararg cmd: String) = try {
+ ProcessBuilder(*cmd).start().waitFor() == 0
+ } catch (_: Exception) { false }
+
+ // 리트코드 exampleTestcases 포맷: 파라미터별 한 줄
+ private val tc = TestCase(input = "[2,7,11,15]\n9", expectedOutput = "[0,1]")
+
+ @Test
+ fun `Rust leetcode impl Solution style`() {
+ if (!available("rustc", "--version")) return
+ // 리트코드 Rust 초기 코드 형태 그대로 (struct Solution 선언 없음 → 래퍼가 보충해야 함)
+ val code = """
+ impl Solution {
+ pub fn two_sum(nums: Vec, target: i32) -> Vec {
+ for i in 0..nums.len() {
+ for j in (i + 1)..nums.len() {
+ if nums[i] + nums[j] == target {
+ return vec![i as i32, j as i32];
+ }
+ }
+ }
+ vec![]
+ }
+ }
+ """.trimIndent()
+ val result = CodeRunner.runProgrammers(code, Language.RUST, tc, listOf("nums", "target"))
+ assertEquals(0, result.exitCode, "compile/run failed: ${result.error}")
+ assertEquals("[0,1]", result.output.trim())
+ }
+
+ @Test
+ fun `Go leetcode twoSum style`() {
+ if (!available("go", "version")) return
+ // 리트코드 Go 초기 코드 형태 그대로 (package 선언 없음, 함수명 twoSum)
+ val code = """
+ func twoSum(nums []int, target int) []int {
+ for i := 0; i < len(nums); i++ {
+ for j := i + 1; j < len(nums); j++ {
+ if nums[i]+nums[j] == target {
+ return []int{i, j}
+ }
+ }
+ }
+ return nil
+ }
+ """.trimIndent()
+ val result = CodeRunner.runProgrammers(code, Language.GO, tc, listOf("nums", "target"))
+ assertEquals(0, result.exitCode, "compile/run failed: ${result.error}")
+ assertEquals("[0,1]", result.output.trim())
+ }
+
+ @Test
+ fun `Ruby leetcode two_sum style`() {
+ if (!available("ruby", "--version")) return
+ // 리트코드 Ruby 초기 코드 형태 그대로
+ val code = """
+ # @param {Integer[]} nums
+ # @param {Integer} target
+ # @return {Integer[]}
+ def two_sum(nums, target)
+ nums.each_with_index do |a, i|
+ nums.each_with_index do |b, j|
+ return [i, j] if i < j && a + b == target
+ end
+ end
+ []
+ end
+ """.trimIndent()
+ val result = CodeRunner.runProgrammers(code, Language.RUBY, tc, listOf("nums", "target"))
+ assertEquals(0, result.exitCode, "run failed: ${result.error}")
+ assertEquals("[0,1]", result.output.trim())
+ }
+
+ @Test
+ fun `String and 2D array literal conversion`() {
+ if (!available("go", "version")) return
+ // 문자열 파라미터 + 문자열 리턴
+ val code = """
+ func greet(name string) string {
+ return "hi " + name
+ }
+ """.trimIndent()
+ val strTc = TestCase(input = "\"world\"", expectedOutput = "\"hi world\"")
+ val result = CodeRunner.runProgrammers(code, Language.GO, strTc, listOf("name"))
+ assertEquals(0, result.exitCode, "run failed: ${result.error}")
+ assertEquals("\"hi world\"", result.output.trim())
+
+ // 2차원 배열 파라미터
+ val code2 = """
+ func sumAll(grid [][]int) int {
+ s := 0
+ for _, row := range grid {
+ for _, v := range row {
+ s += v
+ }
+ }
+ return s
+ }
+ """.trimIndent()
+ val gridTc = TestCase(input = "[[1,2],[3,4]]", expectedOutput = "10")
+ val result2 = CodeRunner.runProgrammers(code2, Language.GO, gridTc, listOf("grid"))
+ assertEquals(0, result2.exitCode, "run failed: ${result2.error}")
+ assertEquals("10", result2.output.trim())
+ }
+}