feat: SQL 코딩 문제 추가 — 브라우저 SQLite(sql.js)로 실행·자동채점
- 문제 5개 시드: SELECT* → WHERE → ORDER BY+LIMIT → GROUP BY → JOIN (직원·부서 연습 데이터, SQL 입문 강좌 진도 순서와 일치) - 실행 방식: sql.js(SQLite WASM)로 학생 브라우저 안 1회용 메모리 DB에서 실행 → 서버 DB 무접촉. DROP/DELETE도 자유롭게 실험 가능 (runJs.js와 같은 격리 철학) - 채점: 값만 비교(컬럼 별칭 허용), ORDER BY 문제만 행 순서 채점(orderMatters) - 화면: 데이터 살펴보기(테이블+데이터), 실행 결과 표 vs 기대 결과 표 나란히 비교 - CodingProblem: setupSql 컬럼, isAutoGraded에 SQL 포함(JAVA만 수동) - CodingServiceTest 3개 추가: SQL 채점값 저장·PASSED, JAVA 채점값 폐기(조작 방지), 빈 코드 400 → 전체 17개 테스트 통과 운영 검증(브라우저 E2E): 정답 실행 '통과', 오답 시 기대 결과 비교 표시, 제출 시 서버에 PASSED 1/1 저장 확인. 문제 수 20→25 재시드. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
cb0fb54185
commit
a5d4437b27
@ -24,6 +24,7 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 이 파일이 하는 일:
|
||||
@ -180,19 +181,23 @@ public class SeedDataLoader implements ApplicationRunner {
|
||||
}
|
||||
int sortOrder = 0;
|
||||
for (CodingProblemSeed seed : seeds) {
|
||||
// 채점 데이터는 언어마다 모양이 다르다 — JS는 테스트케이스 배열, SQL은 기대 결과 객체.
|
||||
// 어느 쪽이든 DB에는 JSON 문자열 하나(testsJson)로 들어간다.
|
||||
String testsJson = null;
|
||||
if (seed.tests() != null) {
|
||||
try {
|
||||
try {
|
||||
if (seed.sqlExpected() != null) {
|
||||
testsJson = objectMapper.writeValueAsString(seed.sqlExpected());
|
||||
} else if (seed.tests() != null) {
|
||||
testsJson = objectMapper.writeValueAsString(seed.tests());
|
||||
} catch (Exception e) {
|
||||
log.error("테스트케이스 직렬화 실패 — 건너뜁니다: {}", seed.title(), e);
|
||||
continue;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("채점 데이터 직렬화 실패 — 건너뜁니다: {}", seed.title(), e);
|
||||
continue;
|
||||
}
|
||||
codingProblemRepository.save(new CodingProblem(
|
||||
seed.language(), seed.title(), seed.difficulty(), seed.topic(),
|
||||
seed.description(), seed.starterCode(), seed.functionName(),
|
||||
testsJson, sortOrder++));
|
||||
seed.setupSql(), testsJson, sortOrder++));
|
||||
}
|
||||
log.info("코딩 문제 시드 {}건을 저장했습니다.", seeds.size());
|
||||
}
|
||||
@ -237,10 +242,14 @@ public class SeedDataLoader implements ApplicationRunner {
|
||||
Integer answerIndex, String explanation) {
|
||||
}
|
||||
|
||||
/** coding-problems.json 한 건 (tests는 JAVA 문제면 null) */
|
||||
/**
|
||||
* coding-problems.json 한 건.
|
||||
* tests는 JS 전용, setupSql·sqlExpected는 SQL 전용, JAVA는 셋 다 null.
|
||||
*/
|
||||
record CodingProblemSeed(String language, String title, String difficulty, String topic,
|
||||
String description, String starterCode, String functionName,
|
||||
List<TestCaseSeed> tests) {
|
||||
List<TestCaseSeed> tests,
|
||||
String setupSql, Map<String, Object> sqlExpected) {
|
||||
}
|
||||
|
||||
/** 테스트케이스 한 건 — args를 함수에 넣었을 때 expected가 나와야 한다 */
|
||||
|
||||
@ -9,6 +9,8 @@ import jakarta.persistence.*;
|
||||
*
|
||||
* 언어(language)에 따라 채점 방식이 다르다:
|
||||
* - JS : 브라우저(Web Worker)가 테스트케이스를 돌려 즉시 통과/실패를 가린다.
|
||||
* - SQL : 브라우저 안의 SQLite(sql.js, WASM)가 쿼리를 실행해 기대 결과와 비교한다.
|
||||
* 서버 DB에는 손끝 하나 닿지 않는다 — 학생마다 자기 브라우저에 1회용 DB가 생긴다.
|
||||
* - JAVA : 실행하지 않는다. 코드를 제출하면 멘토가 직접 읽고 피드백한다.
|
||||
*
|
||||
* 학습 포인트: 왜 자바는 서버에서 실행하지 않을까?
|
||||
@ -26,7 +28,7 @@ public class CodingProblem {
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/** 문제 언어: JS | JAVA */
|
||||
/** 문제 언어: JS | SQL | JAVA */
|
||||
@Column(nullable = false, length = 8)
|
||||
private String language;
|
||||
|
||||
@ -58,8 +60,18 @@ public class CodingProblem {
|
||||
private String functionName;
|
||||
|
||||
/**
|
||||
* 테스트케이스 묶음을 JSON 문자열로 통째로 담는다 (JS 전용, JAVA는 null).
|
||||
* 형식: [{"name":"설명","args":[1,2],"expected":3}, ...]
|
||||
* 문제를 풀 때 브라우저 SQLite에 먼저 실행할 준비 SQL (SQL 전용, 나머지는 null).
|
||||
* CREATE TABLE + INSERT — 즉 "문제의 데이터 세트"가 이 안에 통째로 들어 있다.
|
||||
* 학습 포인트: 같은 준비 SQL을 여러 문제가 복사해서 갖는다. 정규화하면 테이블이 하나 늘고
|
||||
* 조인이 생기는데, 문제 5개 규모에선 "그냥 들고 다니는" 쪽이 단순하다.
|
||||
*/
|
||||
@Column(name = "setup_sql", columnDefinition = "text")
|
||||
private String setupSql;
|
||||
|
||||
/**
|
||||
* 테스트케이스 묶음을 JSON 문자열로 통째로 담는다 (JS·SQL 전용, JAVA는 null).
|
||||
* JS 형식: [{"name":"설명","args":[1,2],"expected":3}, ...]
|
||||
* SQL 형식: {"expectedColumns":[...],"expectedRows":[[...]],"orderMatters":false}
|
||||
*
|
||||
* 학습 포인트: 왜 테스트케이스용 테이블을 따로 만들지 않고 JSON 한 덩어리로 넣을까?
|
||||
* 테스트케이스는 (1) 항상 문제와 함께 통째로 읽고, (2) 따로 검색하거나 조인할 일이 없다.
|
||||
@ -78,7 +90,7 @@ public class CodingProblem {
|
||||
|
||||
public CodingProblem(String language, String title, String difficulty, String topic,
|
||||
String description, String starterCode, String functionName,
|
||||
String testsJson, Integer sortOrder) {
|
||||
String setupSql, String testsJson, Integer sortOrder) {
|
||||
this.language = language;
|
||||
this.title = title;
|
||||
this.difficulty = difficulty;
|
||||
@ -86,13 +98,14 @@ public class CodingProblem {
|
||||
this.description = description;
|
||||
this.starterCode = starterCode;
|
||||
this.functionName = functionName;
|
||||
this.setupSql = setupSql;
|
||||
this.testsJson = testsJson;
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
/** 브라우저에서 자동채점이 가능한 문제인가 (= JS 문제인가) */
|
||||
/** 브라우저에서 자동채점이 가능한 문제인가 (JS와 SQL — JAVA만 멘토 수동 채점) */
|
||||
public boolean isAutoGraded() {
|
||||
return "JS".equals(language);
|
||||
return "JS".equals(language) || "SQL".equals(language);
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
@ -127,6 +140,10 @@ public class CodingProblem {
|
||||
return functionName;
|
||||
}
|
||||
|
||||
public String getSetupSql() {
|
||||
return setupSql;
|
||||
}
|
||||
|
||||
public String getTestsJson() {
|
||||
return testsJson;
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ public class CodingService {
|
||||
}
|
||||
CodingProblem problem = problem(problemId);
|
||||
|
||||
// 자바 문제는 자동채점이 없다 — 브라우저가 뭘 보내오든 채점 값은 버린다.
|
||||
// 자바 문제는 자동채점이 없다 — 브라우저가 뭘 보내오든 채점 값은 버린다. (JS·SQL만 저장)
|
||||
Integer passed = problem.isAutoGraded() ? passedCount : null;
|
||||
Integer total = problem.isAutoGraded() ? totalCount : null;
|
||||
String results = problem.isAutoGraded() ? resultsJson : null;
|
||||
|
||||
@ -54,10 +54,10 @@ public class CodingController {
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** 문제 상세 — 테스트케이스(testsJson)는 브라우저가 채점해야 하므로 함께 내려준다 */
|
||||
/** 문제 상세 — 채점 데이터(testsJson)와 SQL 준비 스크립트(setupSql)는 브라우저가 쓰므로 함께 내려준다 */
|
||||
public record ProblemDetail(Long id, String language, String title, String difficulty,
|
||||
String topic, String description, String starterCode,
|
||||
String functionName, String testsJson,
|
||||
String functionName, String setupSql, String testsJson,
|
||||
String myCode, String status, Integer passedCount,
|
||||
Integer totalCount, String resultsJson, Integer attempts,
|
||||
String feedback) {
|
||||
@ -71,12 +71,14 @@ public class CodingController {
|
||||
return codingService.mySubmission(user, id)
|
||||
.map(s -> new ProblemDetail(problem.getId(), problem.getLanguage(), problem.getTitle(),
|
||||
problem.getDifficulty(), problem.getTopic(), problem.getDescription(),
|
||||
problem.getStarterCode(), problem.getFunctionName(), problem.getTestsJson(),
|
||||
problem.getStarterCode(), problem.getFunctionName(),
|
||||
problem.getSetupSql(), problem.getTestsJson(),
|
||||
s.getCode(), s.getStatus(), s.getPassedCount(), s.getTotalCount(),
|
||||
s.getResultsJson(), s.getAttempts(), s.getFeedback()))
|
||||
.orElseGet(() -> new ProblemDetail(problem.getId(), problem.getLanguage(), problem.getTitle(),
|
||||
problem.getDifficulty(), problem.getTopic(), problem.getDescription(),
|
||||
problem.getStarterCode(), problem.getFunctionName(), problem.getTestsJson(),
|
||||
problem.getStarterCode(), problem.getFunctionName(),
|
||||
problem.getSetupSql(), problem.getTestsJson(),
|
||||
null, "TODO", null, null, null, 0, null));
|
||||
}
|
||||
|
||||
|
||||
@ -8,11 +8,41 @@
|
||||
"starterCode": "function solution(a, b) {\n // 여기에 코드를 작성하세요\n}",
|
||||
"functionName": "solution",
|
||||
"tests": [
|
||||
{ "name": "1 + 2 = 3", "args": [1, 2], "expected": 3 },
|
||||
{ "name": "0 + 0 = 0", "args": [0, 0], "expected": 0 },
|
||||
{ "name": "음수도 된다 (-5 + 3)", "args": [-5, 3], "expected": -2 },
|
||||
{ "name": "큰 수 (10000 + 23456)", "args": [10000, 23456], "expected": 33456 }
|
||||
]
|
||||
{
|
||||
"name": "1 + 2 = 3",
|
||||
"args": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"expected": 3
|
||||
},
|
||||
{
|
||||
"name": "0 + 0 = 0",
|
||||
"args": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"expected": 0
|
||||
},
|
||||
{
|
||||
"name": "음수도 된다 (-5 + 3)",
|
||||
"args": [
|
||||
-5,
|
||||
3
|
||||
],
|
||||
"expected": -2
|
||||
},
|
||||
{
|
||||
"name": "큰 수 (10000 + 23456)",
|
||||
"args": [
|
||||
10000,
|
||||
23456
|
||||
],
|
||||
"expected": 33456
|
||||
}
|
||||
],
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JS",
|
||||
@ -23,11 +53,37 @@
|
||||
"starterCode": "function solution(n) {\n // 여기에 코드를 작성하세요\n}",
|
||||
"functionName": "solution",
|
||||
"tests": [
|
||||
{ "name": "4는 짝수", "args": [4], "expected": true },
|
||||
{ "name": "7은 홀수", "args": [7], "expected": false },
|
||||
{ "name": "0은 짝수", "args": [0], "expected": true },
|
||||
{ "name": "-3은 홀수", "args": [-3], "expected": false }
|
||||
]
|
||||
{
|
||||
"name": "4는 짝수",
|
||||
"args": [
|
||||
4
|
||||
],
|
||||
"expected": true
|
||||
},
|
||||
{
|
||||
"name": "7은 홀수",
|
||||
"args": [
|
||||
7
|
||||
],
|
||||
"expected": false
|
||||
},
|
||||
{
|
||||
"name": "0은 짝수",
|
||||
"args": [
|
||||
0
|
||||
],
|
||||
"expected": true
|
||||
},
|
||||
{
|
||||
"name": "-3은 홀수",
|
||||
"args": [
|
||||
-3
|
||||
],
|
||||
"expected": false
|
||||
}
|
||||
],
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JS",
|
||||
@ -38,11 +94,37 @@
|
||||
"starterCode": "function solution(s) {\n // 여기에 코드를 작성하세요\n}",
|
||||
"functionName": "solution",
|
||||
"tests": [
|
||||
{ "name": "'hello' → 'olleh'", "args": ["hello"], "expected": "olleh" },
|
||||
{ "name": "한 글자는 그대로", "args": ["a"], "expected": "a" },
|
||||
{ "name": "빈 문자열", "args": [""], "expected": "" },
|
||||
{ "name": "한글도 된다", "args": ["가나다"], "expected": "다나가" }
|
||||
]
|
||||
{
|
||||
"name": "'hello' → 'olleh'",
|
||||
"args": [
|
||||
"hello"
|
||||
],
|
||||
"expected": "olleh"
|
||||
},
|
||||
{
|
||||
"name": "한 글자는 그대로",
|
||||
"args": [
|
||||
"a"
|
||||
],
|
||||
"expected": "a"
|
||||
},
|
||||
{
|
||||
"name": "빈 문자열",
|
||||
"args": [
|
||||
""
|
||||
],
|
||||
"expected": ""
|
||||
},
|
||||
{
|
||||
"name": "한글도 된다",
|
||||
"args": [
|
||||
"가나다"
|
||||
],
|
||||
"expected": "다나가"
|
||||
}
|
||||
],
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JS",
|
||||
@ -53,11 +135,51 @@
|
||||
"starterCode": "function solution(numbers) {\n // 여기에 코드를 작성하세요\n}",
|
||||
"functionName": "solution",
|
||||
"tests": [
|
||||
{ "name": "[1, 5, 3] → 5", "args": [[1, 5, 3]], "expected": 5 },
|
||||
{ "name": "원소 하나", "args": [[42]], "expected": 42 },
|
||||
{ "name": "전부 음수", "args": [[-1, -9, -4]], "expected": -1 },
|
||||
{ "name": "중복이 있어도", "args": [[7, 7, 2]], "expected": 7 }
|
||||
]
|
||||
{
|
||||
"name": "[1, 5, 3] → 5",
|
||||
"args": [
|
||||
[
|
||||
1,
|
||||
5,
|
||||
3
|
||||
]
|
||||
],
|
||||
"expected": 5
|
||||
},
|
||||
{
|
||||
"name": "원소 하나",
|
||||
"args": [
|
||||
[
|
||||
42
|
||||
]
|
||||
],
|
||||
"expected": 42
|
||||
},
|
||||
{
|
||||
"name": "전부 음수",
|
||||
"args": [
|
||||
[
|
||||
-1,
|
||||
-9,
|
||||
-4
|
||||
]
|
||||
],
|
||||
"expected": -1
|
||||
},
|
||||
{
|
||||
"name": "중복이 있어도",
|
||||
"args": [
|
||||
[
|
||||
7,
|
||||
7,
|
||||
2
|
||||
]
|
||||
],
|
||||
"expected": 7
|
||||
}
|
||||
],
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JS",
|
||||
@ -68,11 +190,37 @@
|
||||
"starterCode": "function solution(n) {\n // 여기에 코드를 작성하세요\n}",
|
||||
"functionName": "solution",
|
||||
"tests": [
|
||||
{ "name": "n=5 → 15", "args": [5], "expected": 15 },
|
||||
{ "name": "n=1 → 1", "args": [1], "expected": 1 },
|
||||
{ "name": "n=100 → 5050", "args": [100], "expected": 5050 },
|
||||
{ "name": "n=10 → 55", "args": [10], "expected": 55 }
|
||||
]
|
||||
{
|
||||
"name": "n=5 → 15",
|
||||
"args": [
|
||||
5
|
||||
],
|
||||
"expected": 15
|
||||
},
|
||||
{
|
||||
"name": "n=1 → 1",
|
||||
"args": [
|
||||
1
|
||||
],
|
||||
"expected": 1
|
||||
},
|
||||
{
|
||||
"name": "n=100 → 5050",
|
||||
"args": [
|
||||
100
|
||||
],
|
||||
"expected": 5050
|
||||
},
|
||||
{
|
||||
"name": "n=10 → 55",
|
||||
"args": [
|
||||
10
|
||||
],
|
||||
"expected": 55
|
||||
}
|
||||
],
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JS",
|
||||
@ -83,11 +231,37 @@
|
||||
"starterCode": "function solution(s) {\n // 여기에 코드를 작성하세요\n}",
|
||||
"functionName": "solution",
|
||||
"tests": [
|
||||
{ "name": "'hello' → 2", "args": ["hello"], "expected": 2 },
|
||||
{ "name": "모음 없음", "args": ["xyz"], "expected": 0 },
|
||||
{ "name": "전부 모음", "args": ["aeiou"], "expected": 5 },
|
||||
{ "name": "빈 문자열", "args": [""], "expected": 0 }
|
||||
]
|
||||
{
|
||||
"name": "'hello' → 2",
|
||||
"args": [
|
||||
"hello"
|
||||
],
|
||||
"expected": 2
|
||||
},
|
||||
{
|
||||
"name": "모음 없음",
|
||||
"args": [
|
||||
"xyz"
|
||||
],
|
||||
"expected": 0
|
||||
},
|
||||
{
|
||||
"name": "전부 모음",
|
||||
"args": [
|
||||
"aeiou"
|
||||
],
|
||||
"expected": 5
|
||||
},
|
||||
{
|
||||
"name": "빈 문자열",
|
||||
"args": [
|
||||
""
|
||||
],
|
||||
"expected": 0
|
||||
}
|
||||
],
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JS",
|
||||
@ -98,11 +272,54 @@
|
||||
"starterCode": "function solution(numbers) {\n // 여기에 코드를 작성하세요\n}",
|
||||
"functionName": "solution",
|
||||
"tests": [
|
||||
{ "name": "[1,2,3,4] → [2,4]", "args": [[1, 2, 3, 4]], "expected": [2, 4] },
|
||||
{ "name": "짝수 없음", "args": [[1, 3, 5]], "expected": [] },
|
||||
{ "name": "빈 배열", "args": [[]], "expected": [] },
|
||||
{ "name": "0도 짝수", "args": [[0, 1]], "expected": [0] }
|
||||
]
|
||||
{
|
||||
"name": "[1,2,3,4] → [2,4]",
|
||||
"args": [
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
],
|
||||
"expected": [
|
||||
2,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "짝수 없음",
|
||||
"args": [
|
||||
[
|
||||
1,
|
||||
3,
|
||||
5
|
||||
]
|
||||
],
|
||||
"expected": []
|
||||
},
|
||||
{
|
||||
"name": "빈 배열",
|
||||
"args": [
|
||||
[]
|
||||
],
|
||||
"expected": []
|
||||
},
|
||||
{
|
||||
"name": "0도 짝수",
|
||||
"args": [
|
||||
[
|
||||
0,
|
||||
1
|
||||
]
|
||||
],
|
||||
"expected": [
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JS",
|
||||
@ -113,11 +330,65 @@
|
||||
"starterCode": "function solution(n) {\n // 여기에 코드를 작성하세요\n}",
|
||||
"functionName": "solution",
|
||||
"tests": [
|
||||
{ "name": "n=5", "args": [5], "expected": [1, 2, "Fizz", 4, "Buzz"] },
|
||||
{ "name": "n=15 (마지막이 FizzBuzz)", "args": [15], "expected": [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"] },
|
||||
{ "name": "n=1", "args": [1], "expected": [1] },
|
||||
{ "name": "n=3", "args": [3], "expected": [1, 2, "Fizz"] }
|
||||
]
|
||||
{
|
||||
"name": "n=5",
|
||||
"args": [
|
||||
5
|
||||
],
|
||||
"expected": [
|
||||
1,
|
||||
2,
|
||||
"Fizz",
|
||||
4,
|
||||
"Buzz"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "n=15 (마지막이 FizzBuzz)",
|
||||
"args": [
|
||||
15
|
||||
],
|
||||
"expected": [
|
||||
1,
|
||||
2,
|
||||
"Fizz",
|
||||
4,
|
||||
"Buzz",
|
||||
"Fizz",
|
||||
7,
|
||||
8,
|
||||
"Fizz",
|
||||
"Buzz",
|
||||
11,
|
||||
"Fizz",
|
||||
13,
|
||||
14,
|
||||
"FizzBuzz"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "n=1",
|
||||
"args": [
|
||||
1
|
||||
],
|
||||
"expected": [
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "n=3",
|
||||
"args": [
|
||||
3
|
||||
],
|
||||
"expected": [
|
||||
1,
|
||||
2,
|
||||
"Fizz"
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JS",
|
||||
@ -128,11 +399,61 @@
|
||||
"starterCode": "function solution(numbers) {\n // 여기에 코드를 작성하세요\n}",
|
||||
"functionName": "solution",
|
||||
"tests": [
|
||||
{ "name": "[1,2,2,3,1] → [1,2,3]", "args": [[1, 2, 2, 3, 1]], "expected": [1, 2, 3] },
|
||||
{ "name": "중복 없음", "args": [[1, 2, 3]], "expected": [1, 2, 3] },
|
||||
{ "name": "전부 같은 값", "args": [[5, 5, 5]], "expected": [5] },
|
||||
{ "name": "빈 배열", "args": [[]], "expected": [] }
|
||||
]
|
||||
{
|
||||
"name": "[1,2,2,3,1] → [1,2,3]",
|
||||
"args": [
|
||||
[
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
3,
|
||||
1
|
||||
]
|
||||
],
|
||||
"expected": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "중복 없음",
|
||||
"args": [
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
],
|
||||
"expected": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "전부 같은 값",
|
||||
"args": [
|
||||
[
|
||||
5,
|
||||
5,
|
||||
5
|
||||
]
|
||||
],
|
||||
"expected": [
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "빈 배열",
|
||||
"args": [
|
||||
[]
|
||||
],
|
||||
"expected": []
|
||||
}
|
||||
],
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JS",
|
||||
@ -143,11 +464,37 @@
|
||||
"starterCode": "function solution(s) {\n // 여기에 코드를 작성하세요\n}",
|
||||
"functionName": "solution",
|
||||
"tests": [
|
||||
{ "name": "'level' → true", "args": ["level"], "expected": true },
|
||||
{ "name": "'hello' → false", "args": ["hello"], "expected": false },
|
||||
{ "name": "대소문자 무시 'Level'", "args": ["Level"], "expected": true },
|
||||
{ "name": "공백 무시 'a b a'", "args": ["a b a"], "expected": true }
|
||||
]
|
||||
{
|
||||
"name": "'level' → true",
|
||||
"args": [
|
||||
"level"
|
||||
],
|
||||
"expected": true
|
||||
},
|
||||
{
|
||||
"name": "'hello' → false",
|
||||
"args": [
|
||||
"hello"
|
||||
],
|
||||
"expected": false
|
||||
},
|
||||
{
|
||||
"name": "대소문자 무시 'Level'",
|
||||
"args": [
|
||||
"Level"
|
||||
],
|
||||
"expected": true
|
||||
},
|
||||
{
|
||||
"name": "공백 무시 'a b a'",
|
||||
"args": [
|
||||
"a b a"
|
||||
],
|
||||
"expected": true
|
||||
}
|
||||
],
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JS",
|
||||
@ -158,11 +505,46 @@
|
||||
"starterCode": "function solution(sentence) {\n // 여기에 코드를 작성하세요\n}",
|
||||
"functionName": "solution",
|
||||
"tests": [
|
||||
{ "name": "'a b a'", "args": ["a b a"], "expected": { "a": 2, "b": 1 } },
|
||||
{ "name": "단어 하나", "args": ["hello"], "expected": { "hello": 1 } },
|
||||
{ "name": "빈 문자열 → {}", "args": [""], "expected": {} },
|
||||
{ "name": "전부 다른 단어", "args": ["x y z"], "expected": { "x": 1, "y": 1, "z": 1 } }
|
||||
]
|
||||
{
|
||||
"name": "'a b a'",
|
||||
"args": [
|
||||
"a b a"
|
||||
],
|
||||
"expected": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "단어 하나",
|
||||
"args": [
|
||||
"hello"
|
||||
],
|
||||
"expected": {
|
||||
"hello": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "빈 문자열 → {}",
|
||||
"args": [
|
||||
""
|
||||
],
|
||||
"expected": {}
|
||||
},
|
||||
{
|
||||
"name": "전부 다른 단어",
|
||||
"args": [
|
||||
"x y z"
|
||||
],
|
||||
"expected": {
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JS",
|
||||
@ -173,11 +555,51 @@
|
||||
"starterCode": "function solution(numbers) {\n // 여기에 코드를 작성하세요\n}",
|
||||
"functionName": "solution",
|
||||
"tests": [
|
||||
{ "name": "[3,1,3,2] → 2", "args": [[3, 1, 3, 2]], "expected": 2 },
|
||||
{ "name": "전부 같으면 null", "args": [[5, 5, 5]], "expected": null },
|
||||
{ "name": "두 자리 수 주의 [10, 9]", "args": [[10, 9]], "expected": 9 },
|
||||
{ "name": "원소 하나면 null", "args": [[1]], "expected": null }
|
||||
]
|
||||
{
|
||||
"name": "[3,1,3,2] → 2",
|
||||
"args": [
|
||||
[
|
||||
3,
|
||||
1,
|
||||
3,
|
||||
2
|
||||
]
|
||||
],
|
||||
"expected": 2
|
||||
},
|
||||
{
|
||||
"name": "전부 같으면 null",
|
||||
"args": [
|
||||
[
|
||||
5,
|
||||
5,
|
||||
5
|
||||
]
|
||||
],
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"name": "두 자리 수 주의 [10, 9]",
|
||||
"args": [
|
||||
[
|
||||
10,
|
||||
9
|
||||
]
|
||||
],
|
||||
"expected": 9
|
||||
},
|
||||
{
|
||||
"name": "원소 하나면 null",
|
||||
"args": [
|
||||
[
|
||||
1
|
||||
]
|
||||
],
|
||||
"expected": null
|
||||
}
|
||||
],
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JAVA",
|
||||
@ -187,7 +609,9 @@
|
||||
"description": "정수 두 개 중 더 큰 값을 돌려주는 메서드를 완성하세요. 같으면 그 값을 돌려줍니다.\n\n이 문제는 자동채점이 없습니다. 코드를 제출하면 멘토가 직접 읽고 피드백합니다.\n제출 전 IntelliJ에서 직접 실행해 확인해 보세요.\n\n힌트: if문으로 풀어도 되고 Math.max를 써도 됩니다. 둘 다 해보고 어느 쪽이 읽기 좋은지 생각해 보세요.",
|
||||
"starterCode": "public class Solution {\n\n public int solution(int a, int b) {\n // 여기에 코드를 작성하세요\n return 0;\n }\n}",
|
||||
"functionName": null,
|
||||
"tests": null
|
||||
"tests": null,
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JAVA",
|
||||
@ -197,7 +621,9 @@
|
||||
"description": "int 배열의 모든 원소를 더한 값을 돌려주세요. 빈 배열이면 0입니다.\n\n이 문제는 자동채점이 없습니다 — 멘토가 코드를 읽고 피드백합니다.\n\n힌트: 향상된 for문(for (int n : numbers))을 쓰면 인덱스 실수를 줄일 수 있습니다.",
|
||||
"starterCode": "public class Solution {\n\n public int solution(int[] numbers) {\n // 여기에 코드를 작성하세요\n return 0;\n }\n}",
|
||||
"functionName": null,
|
||||
"tests": null
|
||||
"tests": null,
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JAVA",
|
||||
@ -207,7 +633,9 @@
|
||||
"description": "문자열이 null이거나, 비어 있거나, 공백뿐이면 true를 돌려주세요. 그 외에는 false입니다.\n\n예) null → true, \"\" → true, \" \" → true, \" a \" → false\n\n힌트: null 검사를 가장 먼저 해야 합니다. 순서를 바꾸면 NullPointerException이 납니다 —\n왜 그런지 설명할 수 있게 되는 것이 이 문제의 진짜 목표입니다. isBlank()를 찾아보세요.",
|
||||
"starterCode": "public class Solution {\n\n public boolean solution(String s) {\n // 여기에 코드를 작성하세요\n return false;\n }\n}",
|
||||
"functionName": null,
|
||||
"tests": null
|
||||
"tests": null,
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JAVA",
|
||||
@ -217,7 +645,9 @@
|
||||
"description": "1부터 n까지의 정수 중 짝수만 더한 값을 돌려주세요.\n\n예) solution(10) → 30 (2+4+6+8+10)\n\n힌트: for문의 증가식을 i += 2로 두면 짝수만 밟을 수 있습니다.\nif로 걸러내는 방식과 비교해 보고, 어느 쪽이 더 좋은지 근거를 들어 설명해 보세요.",
|
||||
"starterCode": "public class Solution {\n\n public int solution(int n) {\n // 여기에 코드를 작성하세요\n return 0;\n }\n}",
|
||||
"functionName": null,
|
||||
"tests": null
|
||||
"tests": null,
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JAVA",
|
||||
@ -227,7 +657,9 @@
|
||||
"description": "문자열을 거꾸로 뒤집어 돌려주세요.\n\n힌트: StringBuilder에는 reverse()가 있습니다.\n생각해 볼 것 — String을 +로 계속 이어 붙이며 뒤집으면 왜 느릴까요?\n(자바의 String은 한 번 만들면 바뀌지 않는다 = 불변. 이어 붙일 때마다 새 객체가 생깁니다.)\n제출할 때 이 이유를 주석으로 적어 주세요.",
|
||||
"starterCode": "public class Solution {\n\n public String solution(String s) {\n // 여기에 코드를 작성하세요\n return \"\";\n }\n}",
|
||||
"functionName": null,
|
||||
"tests": null
|
||||
"tests": null,
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JAVA",
|
||||
@ -237,7 +669,9 @@
|
||||
"description": "int 배열에서 가장 큰 값을 돌려주세요. 배열은 항상 1개 이상입니다.\n\n힌트: 최댓값의 시작값을 0으로 두면 안 됩니다 — 전부 음수인 배열에서 틀립니다.\n첫 원소(numbers[0])로 시작하세요. 이런 실수를 '경계 조건 버그'라고 부릅니다.",
|
||||
"starterCode": "public class Solution {\n\n public int solution(int[] numbers) {\n // 여기에 코드를 작성하세요\n return 0;\n }\n}",
|
||||
"functionName": null,
|
||||
"tests": null
|
||||
"tests": null,
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JAVA",
|
||||
@ -247,7 +681,9 @@
|
||||
"description": "공백으로 구분된 문장에서 각 단어의 등장 횟수를 Map<String, Integer>로 돌려주세요.\n\n예) \"a b a\" → {a=2, b=1}\n\n힌트: split(\" \")으로 나눈 뒤 Map에 넣습니다.\nmap.getOrDefault(word, 0) + 1 패턴을 찾아보세요 — merge()로도 됩니다.\n빈 문자열일 때 split이 무엇을 돌려주는지 직접 찍어 보고 처리하세요.",
|
||||
"starterCode": "import java.util.HashMap;\nimport java.util.Map;\n\npublic class Solution {\n\n public Map<String, Integer> solution(String sentence) {\n // 여기에 코드를 작성하세요\n return new HashMap<>();\n }\n}",
|
||||
"functionName": null,
|
||||
"tests": null
|
||||
"tests": null,
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "JAVA",
|
||||
@ -257,6 +693,234 @@
|
||||
"description": "Student(name, score) 목록을 점수가 높은 순으로 정렬해 돌려주세요.\n점수가 같으면 이름 오름차순으로 정렬합니다.\n\n힌트: list.sort(Comparator.comparingInt(Student::getScore).reversed().thenComparing(Student::getName))\n메서드 참조(::)와 Comparator 조합을 익히는 문제입니다.\n한 줄로 쓰기 전에, 익명 클래스로 먼저 써 보고 무엇이 줄어드는지 비교해 보세요.",
|
||||
"starterCode": "import java.util.Comparator;\nimport java.util.List;\n\npublic class Solution {\n\n public static class Student {\n private final String name;\n private final int score;\n\n public Student(String name, int score) {\n this.name = name;\n this.score = score;\n }\n\n public String getName() { return name; }\n public int getScore() { return score; }\n }\n\n public List<Student> solution(List<Student> students) {\n // 여기에 코드를 작성하세요\n return students;\n }\n}",
|
||||
"functionName": null,
|
||||
"tests": null
|
||||
"tests": null,
|
||||
"setupSql": null,
|
||||
"sqlExpected": null
|
||||
},
|
||||
{
|
||||
"language": "SQL",
|
||||
"title": "모든 직원 조회하기 (SELECT *)",
|
||||
"difficulty": "EASY",
|
||||
"topic": "sql",
|
||||
"description": "employee 테이블의 모든 행, 모든 컬럼을 조회하세요.\n\n왼쪽 아래 [데이터 살펴보기]에서 테이블 구조와 데이터를 먼저 확인해 보세요.\n\n힌트: SELECT * 는 \"모든 컬럼\"이라는 뜻입니다. FROM 뒤에 테이블 이름을 씁니다.\nSQL 문장 끝에는 세미콜론(;)을 붙이는 습관을 들이세요.",
|
||||
"starterCode": "-- 여기에 SQL을 작성하세요\nSELECT ",
|
||||
"functionName": null,
|
||||
"tests": null,
|
||||
"setupSql": "CREATE TABLE department (\n id INTEGER PRIMARY KEY, -- 부서 번호\n name TEXT NOT NULL -- 부서 이름\n);\n\nCREATE TABLE employee (\n id INTEGER PRIMARY KEY, -- 사번\n name TEXT NOT NULL, -- 이름\n dept_id INTEGER NOT NULL, -- 소속 부서 번호 (department.id)\n salary INTEGER NOT NULL, -- 연봉 (만원)\n joined_year INTEGER NOT NULL -- 입사 연도\n);\n\nINSERT INTO department (id, name) VALUES\n (1, '개발팀'), (2, '디자인팀'), (3, '경영지원팀');\n\nINSERT INTO employee (id, name, dept_id, salary, joined_year) VALUES\n (1, '김하늘', 1, 3200, 2023),\n (2, '이준호', 1, 4100, 2021),\n (3, '박서연', 2, 3600, 2022),\n (4, '최민준', 1, 2900, 2024),\n (5, '정유진', 2, 3300, 2023),\n (6, '한지우', 3, 3800, 2020),\n (7, '오세훈', 3, 2700, 2024),\n (8, '서지민', 1, 4500, 2019);",
|
||||
"sqlExpected": {
|
||||
"expectedColumns": [
|
||||
"id",
|
||||
"name",
|
||||
"dept_id",
|
||||
"salary",
|
||||
"joined_year"
|
||||
],
|
||||
"expectedRows": [
|
||||
[
|
||||
1,
|
||||
"김하늘",
|
||||
1,
|
||||
3200,
|
||||
2023
|
||||
],
|
||||
[
|
||||
2,
|
||||
"이준호",
|
||||
1,
|
||||
4100,
|
||||
2021
|
||||
],
|
||||
[
|
||||
3,
|
||||
"박서연",
|
||||
2,
|
||||
3600,
|
||||
2022
|
||||
],
|
||||
[
|
||||
4,
|
||||
"최민준",
|
||||
1,
|
||||
2900,
|
||||
2024
|
||||
],
|
||||
[
|
||||
5,
|
||||
"정유진",
|
||||
2,
|
||||
3300,
|
||||
2023
|
||||
],
|
||||
[
|
||||
6,
|
||||
"한지우",
|
||||
3,
|
||||
3800,
|
||||
2020
|
||||
],
|
||||
[
|
||||
7,
|
||||
"오세훈",
|
||||
3,
|
||||
2700,
|
||||
2024
|
||||
],
|
||||
[
|
||||
8,
|
||||
"서지민",
|
||||
1,
|
||||
4500,
|
||||
2019
|
||||
]
|
||||
],
|
||||
"orderMatters": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"language": "SQL",
|
||||
"title": "조건으로 거르기 (WHERE)",
|
||||
"difficulty": "EASY",
|
||||
"topic": "sql",
|
||||
"description": "연봉(salary)이 3500만원 이상인 직원의 이름(name)만 조회하세요.\n\n힌트: WHERE 절이 \"행을 거르는 조건\"입니다. 이상은 >= 로 씁니다.\n필요한 컬럼(name)만 SELECT 하는 것도 문제의 일부예요 — *를 쓰면 컬럼이 남아서 틀립니다.",
|
||||
"starterCode": "-- 연봉 3500 이상 직원의 이름만\nSELECT ",
|
||||
"functionName": null,
|
||||
"tests": null,
|
||||
"setupSql": "CREATE TABLE department (\n id INTEGER PRIMARY KEY, -- 부서 번호\n name TEXT NOT NULL -- 부서 이름\n);\n\nCREATE TABLE employee (\n id INTEGER PRIMARY KEY, -- 사번\n name TEXT NOT NULL, -- 이름\n dept_id INTEGER NOT NULL, -- 소속 부서 번호 (department.id)\n salary INTEGER NOT NULL, -- 연봉 (만원)\n joined_year INTEGER NOT NULL -- 입사 연도\n);\n\nINSERT INTO department (id, name) VALUES\n (1, '개발팀'), (2, '디자인팀'), (3, '경영지원팀');\n\nINSERT INTO employee (id, name, dept_id, salary, joined_year) VALUES\n (1, '김하늘', 1, 3200, 2023),\n (2, '이준호', 1, 4100, 2021),\n (3, '박서연', 2, 3600, 2022),\n (4, '최민준', 1, 2900, 2024),\n (5, '정유진', 2, 3300, 2023),\n (6, '한지우', 3, 3800, 2020),\n (7, '오세훈', 3, 2700, 2024),\n (8, '서지민', 1, 4500, 2019);",
|
||||
"sqlExpected": {
|
||||
"expectedColumns": [
|
||||
"name"
|
||||
],
|
||||
"expectedRows": [
|
||||
[
|
||||
"이준호"
|
||||
],
|
||||
[
|
||||
"박서연"
|
||||
],
|
||||
[
|
||||
"한지우"
|
||||
],
|
||||
[
|
||||
"서지민"
|
||||
]
|
||||
],
|
||||
"orderMatters": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"language": "SQL",
|
||||
"title": "정렬해서 상위 3명만 (ORDER BY + LIMIT)",
|
||||
"difficulty": "NORMAL",
|
||||
"topic": "sql",
|
||||
"description": "연봉이 높은 순서대로 상위 3명의 이름(name)과 연봉(salary)을 조회하세요.\n\n이 문제는 순서까지 채점합니다 — 연봉이 높은 사람이 첫 줄에 와야 해요.\n\n힌트: ORDER BY salary DESC 가 \"연봉 내림차순 정렬\"입니다.\nLIMIT 3 을 붙이면 앞에서 3행만 가져옵니다. (실무에서 \"톱 N\" 조회의 기본 패턴)",
|
||||
"starterCode": "-- 연봉 상위 3명: 이름, 연봉 (높은 순)\nSELECT ",
|
||||
"functionName": null,
|
||||
"tests": null,
|
||||
"setupSql": "CREATE TABLE department (\n id INTEGER PRIMARY KEY, -- 부서 번호\n name TEXT NOT NULL -- 부서 이름\n);\n\nCREATE TABLE employee (\n id INTEGER PRIMARY KEY, -- 사번\n name TEXT NOT NULL, -- 이름\n dept_id INTEGER NOT NULL, -- 소속 부서 번호 (department.id)\n salary INTEGER NOT NULL, -- 연봉 (만원)\n joined_year INTEGER NOT NULL -- 입사 연도\n);\n\nINSERT INTO department (id, name) VALUES\n (1, '개발팀'), (2, '디자인팀'), (3, '경영지원팀');\n\nINSERT INTO employee (id, name, dept_id, salary, joined_year) VALUES\n (1, '김하늘', 1, 3200, 2023),\n (2, '이준호', 1, 4100, 2021),\n (3, '박서연', 2, 3600, 2022),\n (4, '최민준', 1, 2900, 2024),\n (5, '정유진', 2, 3300, 2023),\n (6, '한지우', 3, 3800, 2020),\n (7, '오세훈', 3, 2700, 2024),\n (8, '서지민', 1, 4500, 2019);",
|
||||
"sqlExpected": {
|
||||
"expectedColumns": [
|
||||
"name",
|
||||
"salary"
|
||||
],
|
||||
"expectedRows": [
|
||||
[
|
||||
"서지민",
|
||||
4500
|
||||
],
|
||||
[
|
||||
"이준호",
|
||||
4100
|
||||
],
|
||||
[
|
||||
"한지우",
|
||||
3800
|
||||
]
|
||||
],
|
||||
"orderMatters": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"language": "SQL",
|
||||
"title": "부서별로 묶어서 세기 (GROUP BY)",
|
||||
"difficulty": "NORMAL",
|
||||
"topic": "sql",
|
||||
"description": "부서 번호(dept_id)별로 직원이 몇 명인지 조회하세요.\n결과 컬럼: dept_id, 인원 수 — 2개 컬럼이어야 합니다.\n\n힌트: GROUP BY dept_id 는 \"부서 번호가 같은 행끼리 묶어라\"입니다.\n묶고 나면 COUNT(*)로 각 묶음의 행 수를 셀 수 있어요.\nWHERE는 묶기 전에 거르고, 집계 결과로 거르려면 HAVING을 씁니다 — 차이를 기억해 두세요.",
|
||||
"starterCode": "-- 부서 번호별 직원 수\nSELECT ",
|
||||
"functionName": null,
|
||||
"tests": null,
|
||||
"setupSql": "CREATE TABLE department (\n id INTEGER PRIMARY KEY, -- 부서 번호\n name TEXT NOT NULL -- 부서 이름\n);\n\nCREATE TABLE employee (\n id INTEGER PRIMARY KEY, -- 사번\n name TEXT NOT NULL, -- 이름\n dept_id INTEGER NOT NULL, -- 소속 부서 번호 (department.id)\n salary INTEGER NOT NULL, -- 연봉 (만원)\n joined_year INTEGER NOT NULL -- 입사 연도\n);\n\nINSERT INTO department (id, name) VALUES\n (1, '개발팀'), (2, '디자인팀'), (3, '경영지원팀');\n\nINSERT INTO employee (id, name, dept_id, salary, joined_year) VALUES\n (1, '김하늘', 1, 3200, 2023),\n (2, '이준호', 1, 4100, 2021),\n (3, '박서연', 2, 3600, 2022),\n (4, '최민준', 1, 2900, 2024),\n (5, '정유진', 2, 3300, 2023),\n (6, '한지우', 3, 3800, 2020),\n (7, '오세훈', 3, 2700, 2024),\n (8, '서지민', 1, 4500, 2019);",
|
||||
"sqlExpected": {
|
||||
"expectedColumns": [
|
||||
"dept_id",
|
||||
"count"
|
||||
],
|
||||
"expectedRows": [
|
||||
[
|
||||
1,
|
||||
4
|
||||
],
|
||||
[
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
3,
|
||||
2
|
||||
]
|
||||
],
|
||||
"orderMatters": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"language": "SQL",
|
||||
"title": "두 테이블 이어 붙이기 (JOIN)",
|
||||
"difficulty": "HARD",
|
||||
"topic": "sql",
|
||||
"description": "모든 직원의 이름과 \"부서 이름\"을 나란히 조회하세요.\n결과 컬럼: 직원 이름, 부서 이름 — 2개 컬럼이어야 합니다.\n\n부서 이름은 employee에 없고 department에 있습니다. 두 테이블을 이어야 해요.\n\n힌트: JOIN department ON employee.dept_id = department.id\n두 테이블에 같은 이름의 컬럼(name)이 있으니 employee.name 처럼 테이블을 붙여 구분하세요.\n테이블 이름이 길면 FROM employee e 처럼 별칭(alias)을 쓸 수 있습니다.",
|
||||
"starterCode": "-- 직원 이름 + 부서 이름\nSELECT ",
|
||||
"functionName": null,
|
||||
"tests": null,
|
||||
"setupSql": "CREATE TABLE department (\n id INTEGER PRIMARY KEY, -- 부서 번호\n name TEXT NOT NULL -- 부서 이름\n);\n\nCREATE TABLE employee (\n id INTEGER PRIMARY KEY, -- 사번\n name TEXT NOT NULL, -- 이름\n dept_id INTEGER NOT NULL, -- 소속 부서 번호 (department.id)\n salary INTEGER NOT NULL, -- 연봉 (만원)\n joined_year INTEGER NOT NULL -- 입사 연도\n);\n\nINSERT INTO department (id, name) VALUES\n (1, '개발팀'), (2, '디자인팀'), (3, '경영지원팀');\n\nINSERT INTO employee (id, name, dept_id, salary, joined_year) VALUES\n (1, '김하늘', 1, 3200, 2023),\n (2, '이준호', 1, 4100, 2021),\n (3, '박서연', 2, 3600, 2022),\n (4, '최민준', 1, 2900, 2024),\n (5, '정유진', 2, 3300, 2023),\n (6, '한지우', 3, 3800, 2020),\n (7, '오세훈', 3, 2700, 2024),\n (8, '서지민', 1, 4500, 2019);",
|
||||
"sqlExpected": {
|
||||
"expectedColumns": [
|
||||
"name",
|
||||
"name"
|
||||
],
|
||||
"expectedRows": [
|
||||
[
|
||||
"김하늘",
|
||||
"개발팀"
|
||||
],
|
||||
[
|
||||
"이준호",
|
||||
"개발팀"
|
||||
],
|
||||
[
|
||||
"박서연",
|
||||
"디자인팀"
|
||||
],
|
||||
[
|
||||
"최민준",
|
||||
"개발팀"
|
||||
],
|
||||
[
|
||||
"정유진",
|
||||
"디자인팀"
|
||||
],
|
||||
[
|
||||
"한지우",
|
||||
"경영지원팀"
|
||||
],
|
||||
[
|
||||
"오세훈",
|
||||
"경영지원팀"
|
||||
],
|
||||
[
|
||||
"서지민",
|
||||
"개발팀"
|
||||
]
|
||||
],
|
||||
"orderMatters": false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -0,0 +1,93 @@
|
||||
package dev.awesomedev.mirim.service;
|
||||
|
||||
import dev.awesomedev.mirim.domain.CodingProblem;
|
||||
import dev.awesomedev.mirim.domain.CodingSubmission;
|
||||
import dev.awesomedev.mirim.domain.User;
|
||||
import dev.awesomedev.mirim.repository.CodingProblemRepository;
|
||||
import dev.awesomedev.mirim.repository.CodingSubmissionRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 이 파일이 하는 일:
|
||||
* CodingService의 제출 규칙을 검증한다 — 특히 "언어별 채점 값 처리"가 핵심 규칙이다.
|
||||
*
|
||||
* 검증하는 규칙:
|
||||
* (1) 자동채점 언어(JS·SQL)는 브라우저가 보낸 채점 값을 저장하고, 다 맞으면 PASSED.
|
||||
* (2) 수동채점 언어(JAVA)는 브라우저가 뭘 보내오든 채점 값을 버린다 —
|
||||
* 조작된 "다 통과" 값이 성적처럼 저장되면 안 되니까.
|
||||
* (3) 빈 코드는 400.
|
||||
*/
|
||||
class CodingServiceTest {
|
||||
|
||||
private CodingProblemRepository codingProblemRepository;
|
||||
private CodingSubmissionRepository codingSubmissionRepository;
|
||||
private CodingService codingService;
|
||||
|
||||
private User student;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
codingProblemRepository = mock(CodingProblemRepository.class);
|
||||
codingSubmissionRepository = mock(CodingSubmissionRepository.class);
|
||||
codingService = new CodingService(codingProblemRepository, codingSubmissionRepository);
|
||||
student = new User("student1", "해시", "수습생1", "STUDENT", "DEV");
|
||||
}
|
||||
|
||||
private CodingProblem problemOf(String language) {
|
||||
return new CodingProblem(language, "문제", "EASY", "topic", "설명", "뼈대", null, null, null, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SQL 문제: 채점 값이 저장되고, 다 맞으면 상태가 PASSED가 된다")
|
||||
void submit_sql_keeps_grading_and_passes() {
|
||||
CodingProblem problem = problemOf("SQL");
|
||||
when(codingProblemRepository.findById(1L)).thenReturn(Optional.of(problem));
|
||||
when(codingSubmissionRepository.findByProblemAndUser(problem, student)).thenReturn(Optional.empty());
|
||||
when(codingSubmissionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
codingService.submit(student, 1L, "SELECT 1", 1, 1, "[{\"passed\":true}]");
|
||||
|
||||
ArgumentCaptor<CodingSubmission> captor = ArgumentCaptor.forClass(CodingSubmission.class);
|
||||
verify(codingSubmissionRepository).save(captor.capture());
|
||||
CodingSubmission saved = captor.getValue();
|
||||
assertEquals(1, saved.getPassedCount());
|
||||
assertEquals("PASSED", saved.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("JAVA 문제: 브라우저가 '다 통과'라고 보내와도 채점 값을 버린다 (조작 방지)")
|
||||
void submit_java_discards_grading() {
|
||||
CodingProblem problem = problemOf("JAVA");
|
||||
when(codingProblemRepository.findById(1L)).thenReturn(Optional.of(problem));
|
||||
when(codingSubmissionRepository.findByProblemAndUser(problem, student)).thenReturn(Optional.empty());
|
||||
when(codingSubmissionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
// 개발자도구로 요청을 조작해 "5/5 통과"라고 보낸 상황
|
||||
codingService.submit(student, 1L, "class Solution {}", 5, 5, "[]");
|
||||
|
||||
ArgumentCaptor<CodingSubmission> captor = ArgumentCaptor.forClass(CodingSubmission.class);
|
||||
verify(codingSubmissionRepository).save(captor.capture());
|
||||
CodingSubmission saved = captor.getValue();
|
||||
assertNull(saved.getPassedCount()); // 채점 값은 버려졌다
|
||||
assertEquals("SUBMITTED", saved.getStatus()); // PASSED가 아니라 멘토 확인 대기
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("빈 코드를 제출하면 400을 던진다")
|
||||
void submit_rejects_blank_code() {
|
||||
ResponseStatusException ex = assertThrows(ResponseStatusException.class,
|
||||
() -> codingService.submit(student, 1L, " ", null, null, null));
|
||||
|
||||
assertEquals(400, ex.getStatusCode().value());
|
||||
verify(codingSubmissionRepository, never()).save(any());
|
||||
}
|
||||
}
|
||||
9
frontend/package-lock.json
generated
9
frontend/package-lock.json
generated
@ -11,7 +11,8 @@
|
||||
"axios": "^1.7.9",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.0"
|
||||
"react-router-dom": "^7.1.0",
|
||||
"sql.js": "^1.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
@ -2049,6 +2050,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sql.js": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz",
|
||||
"integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
|
||||
@ -12,7 +12,8 @@
|
||||
"axios": "^1.7.9",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.0"
|
||||
"react-router-dom": "^7.1.0",
|
||||
"sql.js": "^1.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
|
||||
@ -171,7 +171,7 @@ export default function MentorCodingSection() {
|
||||
<td>{s.problemTitle}</td>
|
||||
<td>
|
||||
<span className={`lang-badge lang-badge--${s.language.toLowerCase()}`}>
|
||||
{s.language === 'JS' ? 'JavaScript' : 'Java'}
|
||||
{{ JS: 'JavaScript', SQL: 'SQL', JAVA: 'Java' }[s.language] ?? s.language}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ textAlign: 'center', fontVariantNumeric: 'tabular-nums' }}>
|
||||
|
||||
91
frontend/src/lib/runSql.js
Normal file
91
frontend/src/lib/runSql.js
Normal file
@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 이 파일이 하는 일:
|
||||
* 학생이 작성한 SQL을 브라우저 안에서 실행하고, 기대 결과와 비교해 채점한다.
|
||||
*
|
||||
* 핵심 아이디어 — sql.js (SQLite를 WebAssembly로 컴파일한 것).
|
||||
* 학습 포인트: "SQL을 실행한다"고 하면 서버 DB부터 떠올리기 쉽지만, 그건 위험하다.
|
||||
* 학생 SQL에 DROP TABLE이 들어 있으면? 서버가 학생마다 DB를 만들어 줘야 하나?
|
||||
* sql.js는 진짜 SQLite 엔진을 브라우저 안에서 돌린다. 문제를 열 때마다
|
||||
* 메모리 위에 1회용 DB를 새로 만들고, 닫으면 사라진다.
|
||||
* → 무엇을 실행하든 서버에는 아무 일도 일어나지 않는다. DROP도 DELETE도 자유.
|
||||
* → runJs.js(Web Worker)와 같은 철학이다: "남의 코드는 그 사람의 브라우저에서만".
|
||||
*
|
||||
* JS 채점(runJs.js)과 달리 Web Worker를 쓰지 않는 이유:
|
||||
* SELECT는 코드가 아니라 질의라서 무한루프를 만들기가 어렵고(재귀 CTE 정도가 예외),
|
||||
* 데이터가 몇 줄 안 돼 실행이 순식간이다. Worker로 감싸는 복잡함이 이득보다 크다.
|
||||
* — 같은 문제(격리)라도 위험의 크기에 따라 장치의 무게를 달리한다.
|
||||
*/
|
||||
import initSqlJs from 'sql.js';
|
||||
// 학습 포인트: ?url 은 Vite 문법 — "이 파일을 번들에 포함하고, 그 주소(URL)를 달라".
|
||||
// WASM은 JS처럼 import할 수 없어서, 주소를 sql.js에 알려 주고 직접 내려받게 한다.
|
||||
import sqlWasmUrl from 'sql.js/dist/sql-wasm.wasm?url';
|
||||
|
||||
/** WASM 로딩은 느리다(수백 KB) — 한 번만 하고 계속 재사용한다(캐싱). */
|
||||
let sqlJsPromise = null;
|
||||
|
||||
function loadSqlJs() {
|
||||
if (!sqlJsPromise) {
|
||||
sqlJsPromise = initSqlJs({ locateFile: () => sqlWasmUrl });
|
||||
}
|
||||
return sqlJsPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* 준비 SQL(테이블+데이터)을 깐 1회용 DB에서 학생 쿼리를 실행한다.
|
||||
*
|
||||
* @param {string} setupSql CREATE TABLE + INSERT (문제가 제공)
|
||||
* @param {string} query 학생이 작성한 SQL
|
||||
* @returns {Promise<{columns?: string[], rows?: Array[], error?: string}>}
|
||||
*/
|
||||
export async function runSql(setupSql, query) {
|
||||
const SQL = await loadSqlJs();
|
||||
const db = new SQL.Database(); // 메모리 위 1회용 DB — 새 것으로 시작
|
||||
try {
|
||||
db.run(setupSql);
|
||||
// exec는 [{columns, values}] 배열을 돌려준다. 문장이 여러 개면 결과도 여러 개인데,
|
||||
// 채점은 "마지막 SELECT의 결과"로 한다 — 학생이 확인용 문장을 앞에 써도 괜찮도록.
|
||||
const results = db.exec(query);
|
||||
if (results.length === 0) {
|
||||
return { error: '결과가 없습니다. SELECT 문을 작성했는지 확인해 보세요.' };
|
||||
}
|
||||
const last = results[results.length - 1];
|
||||
return { columns: last.columns, rows: last.values };
|
||||
} catch (e) {
|
||||
// SQLite의 에러 메시지는 영어지만 원문 그대로 보여준다 —
|
||||
// "near \"FORM\": syntax error" 같은 메시지를 읽어내는 것도 배워야 할 기술이다.
|
||||
return { error: 'SQL 오류: ' + e.message };
|
||||
} finally {
|
||||
db.close(); // WASM 메모리는 가비지 컬렉터가 못 치운다 — 반드시 직접 닫는다.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 실행 결과를 기대 결과와 비교한다.
|
||||
*
|
||||
* 채점 규칙:
|
||||
* - 값만 비교한다. 컬럼 "이름"은 비교하지 않는다 — SELECT salary AS 연봉 처럼
|
||||
* 별칭을 붙여도 정답이다. (컬럼 개수가 다르면 행 내용이 달라져 어차피 틀린다)
|
||||
* - orderMatters가 false면 행 순서는 무시한다. ORDER BY를 요구하는 문제만 true로 둔다.
|
||||
*
|
||||
* 학습 포인트: 행 순서를 무시하는 비교는 "정렬해서 비교"로 구현한다.
|
||||
* 두 결과를 같은 규칙으로 정렬해 놓으면, 내용이 같은지 순서와 무관하게 확인할 수 있다.
|
||||
*
|
||||
* @param {{columns: string[], rows: Array[]}} actual 학생 쿼리의 실행 결과
|
||||
* @param {{expectedRows: Array[], orderMatters: boolean}} expected 문제의 기대 결과
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function gradeSql(actual, expected) {
|
||||
// 행을 JSON 문자열로 바꿔 비교한다 — runJs.js의 배열 비교와 같은 요령이다.
|
||||
const normalize = (rows) => rows.map((row) => JSON.stringify(row));
|
||||
const actualRows = normalize(actual.rows);
|
||||
const expectedRows = normalize(expected.expectedRows);
|
||||
|
||||
if (actualRows.length !== expectedRows.length) {
|
||||
return false;
|
||||
}
|
||||
if (!expected.orderMatters) {
|
||||
actualRows.sort();
|
||||
expectedRows.sort();
|
||||
}
|
||||
return actualRows.every((row, i) => row === expectedRows[i]);
|
||||
}
|
||||
@ -89,7 +89,7 @@ export default function CodingPage() {
|
||||
<div className="coding-item__title">{problem.title}</div>
|
||||
<div className="coding-item__meta">
|
||||
<span className={`lang-badge lang-badge--${problem.language.toLowerCase()}`}>
|
||||
{problem.language === 'JS' ? 'JavaScript' : 'Java'}
|
||||
{{ JS: 'JavaScript', SQL: 'SQL', JAVA: 'Java' }[problem.language] ?? problem.language}
|
||||
</span>
|
||||
<span className="coding-item__difficulty">
|
||||
{DIFFICULTY_LABEL[problem.difficulty] ?? problem.difficulty}
|
||||
|
||||
@ -3,14 +3,39 @@
|
||||
// 언어별로 흐름이 다르다:
|
||||
// - JS : [실행] 누르면 브라우저(Web Worker)가 테스트를 돌려 즉시 통과/실패를 보여준다.
|
||||
// [제출]하면 코드 + 채점 결과가 서버에 저장되고 멘토가 본다.
|
||||
// - SQL : [실행] 누르면 브라우저 안의 SQLite(sql.js)가 쿼리를 돌려 결과 표를 보여주고,
|
||||
// 기대 결과와 비교해 통과/실패를 가린다. 서버 DB는 전혀 건드리지 않는다.
|
||||
// - JAVA : 실행 버튼이 없다. 코드를 제출하면 멘토가 읽고 피드백한다.
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import client from '../api/client';
|
||||
import CodeEditor from '../components/CodeEditor';
|
||||
import { runJs } from '../lib/runJs';
|
||||
import { runSql, gradeSql } from '../lib/runSql';
|
||||
|
||||
const DIFFICULTY_LABEL = { EASY: '쉬움', NORMAL: '보통', HARD: '어려움' };
|
||||
const LANGUAGE_LABEL = { JS: 'JavaScript', SQL: 'SQL', JAVA: 'Java' };
|
||||
|
||||
/** SQL 실행 결과를 표로 보여주는 작은 부품 (실행 결과·기대 결과 양쪽에서 쓴다) */
|
||||
function SqlResultTable({ columns, rows }) {
|
||||
return (
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table className="sql-table">
|
||||
<thead>
|
||||
<tr>{columns.map((c, i) => <th key={i}>{c}</th>)}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, i) => (
|
||||
<tr key={i}>
|
||||
{row.map((cell, j) => <td key={j}>{cell === null ? 'NULL' : String(cell)}</td>)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<p className="muted" style={{ fontSize: 12.5, marginTop: 4 }}>{rows.length}행</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CodingProblemPage() {
|
||||
const { id } = useParams();
|
||||
@ -18,6 +43,7 @@ export default function CodingProblemPage() {
|
||||
const [problem, setProblem] = useState(null);
|
||||
const [code, setCode] = useState('');
|
||||
const [results, setResults] = useState(null); // 실행 결과 [{name, passed, expected, actual}]
|
||||
const [sqlOutput, setSqlOutput] = useState(null); // SQL 실행 결과 표 {columns, rows, passed}
|
||||
const [runError, setRunError] = useState(''); // 문법 오류·타임아웃 등
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [running, setRunning] = useState(false);
|
||||
@ -57,13 +83,49 @@ export default function CodingProblemPage() {
|
||||
}, [id]);
|
||||
|
||||
const isJs = problem?.language === 'JS';
|
||||
const tests = problem?.testsJson ? JSON.parse(problem.testsJson) : [];
|
||||
const isSql = problem?.language === 'SQL';
|
||||
// testsJson은 언어마다 모양이 다르다 — JS는 테스트 배열, SQL은 기대 결과 객체
|
||||
const tests = isJs && problem?.testsJson ? JSON.parse(problem.testsJson) : [];
|
||||
const sqlExpected = isSql && problem?.testsJson ? JSON.parse(problem.testsJson) : null;
|
||||
|
||||
/**
|
||||
* SQL을 실행하고 채점한다. [실행]과 [제출] 양쪽에서 쓴다.
|
||||
* 결과 요약은 JS와 같은 형식([{name, passed, ...}])으로도 만든다 —
|
||||
* 서버 저장·멘토 화면·재방문 복원이 언어 구분 없이 한 가지 형식으로 굴러가게 하기 위해서다.
|
||||
*/
|
||||
async function runAndGradeSql() {
|
||||
const outcome = await runSql(problem.setupSql, code);
|
||||
if (outcome.error) {
|
||||
return { error: outcome.error };
|
||||
}
|
||||
const passed = gradeSql(outcome, sqlExpected);
|
||||
const summary = [{
|
||||
name: '쿼리 결과가 기대 결과와 일치',
|
||||
passed,
|
||||
expected: `${sqlExpected.expectedRows.length}행`,
|
||||
actual: `${outcome.rows.length}행`,
|
||||
}];
|
||||
return { outcome: { ...outcome, passed }, summary };
|
||||
}
|
||||
|
||||
/** [실행] — 브라우저에서 테스트를 돌린다 (서버에 저장하지 않는다) */
|
||||
async function handleRun() {
|
||||
setRunning(true);
|
||||
setRunError('');
|
||||
setMessage('');
|
||||
if (isSql) {
|
||||
const { error: sqlError, outcome, summary } = await runAndGradeSql();
|
||||
if (sqlError) {
|
||||
setRunError(sqlError);
|
||||
setSqlOutput(null);
|
||||
setResults(null);
|
||||
} else {
|
||||
setSqlOutput(outcome);
|
||||
setResults(summary);
|
||||
}
|
||||
setRunning(false);
|
||||
return;
|
||||
}
|
||||
const outcome = await runJs(code, problem.functionName, tests);
|
||||
if (outcome.error) {
|
||||
setRunError(outcome.error);
|
||||
@ -82,7 +144,24 @@ export default function CodingProblemPage() {
|
||||
try {
|
||||
let payload = { code, passedCount: null, totalCount: null, resultsJson: null };
|
||||
|
||||
if (isJs) {
|
||||
if (isSql) {
|
||||
const { error: sqlError, outcome, summary } = await runAndGradeSql();
|
||||
if (sqlError) {
|
||||
// SQL이 아예 안 도는 상태로는 제출하지 않는다 — JS의 문법 오류 차단과 같은 이유.
|
||||
setRunError(sqlError);
|
||||
setSqlOutput(null);
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
setSqlOutput(outcome);
|
||||
setResults(summary);
|
||||
payload = {
|
||||
code,
|
||||
passedCount: outcome.passed ? 1 : 0,
|
||||
totalCount: 1,
|
||||
resultsJson: JSON.stringify(summary),
|
||||
};
|
||||
} else if (isJs) {
|
||||
const outcome = await runJs(code, problem.functionName, tests);
|
||||
if (outcome.error) {
|
||||
// 문법 오류가 있으면 제출을 막는다 — 멘토에게 안 돌아가는 코드를 보내지 않도록.
|
||||
@ -133,7 +212,7 @@ export default function CodingProblemPage() {
|
||||
<section className="hero compact">
|
||||
<div className="chip-row">
|
||||
<span className={`lang-badge lang-badge--${problem.language.toLowerCase()}`}>
|
||||
{isJs ? 'JavaScript' : 'Java'}
|
||||
{LANGUAGE_LABEL[problem.language] ?? problem.language}
|
||||
</span>
|
||||
<span className="coding-item__difficulty">
|
||||
{DIFFICULTY_LABEL[problem.difficulty] ?? problem.difficulty}
|
||||
@ -148,12 +227,24 @@ export default function CodingProblemPage() {
|
||||
<p className="coding-description">{problem.description}</p>
|
||||
</div>
|
||||
|
||||
{/* SQL 문제: 테이블 구조와 데이터를 먼저 보여준다 — 데이터를 모르고 쿼리를 쓸 수는 없다 */}
|
||||
{isSql && problem.setupSql && (
|
||||
<div className="card">
|
||||
<details>
|
||||
<summary style={{ cursor: 'pointer', fontWeight: 700 }}>
|
||||
데이터 살펴보기 (테이블 구조 + 들어 있는 데이터)
|
||||
</summary>
|
||||
<pre className="sql-setup">{problem.setupSql}</pre>
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card">
|
||||
<h2>코드 작성</h2>
|
||||
<CodeEditor value={code} onChange={setCode} />
|
||||
|
||||
<div className="coding-actions">
|
||||
{isJs && (
|
||||
{(isJs || isSql) && (
|
||||
<button type="button" className="btn" onClick={handleRun} disabled={running || submitting}>
|
||||
{running ? '실행 중…' : '실행해 보기'}
|
||||
</button>
|
||||
@ -172,6 +263,7 @@ export default function CodingProblemPage() {
|
||||
onClick={() => {
|
||||
setCode(problem.starterCode);
|
||||
setResults(null);
|
||||
setSqlOutput(null);
|
||||
setRunError('');
|
||||
}}
|
||||
disabled={submitting || running}
|
||||
@ -185,8 +277,34 @@ export default function CodingProblemPage() {
|
||||
{error && <p className="error-text">{error}</p>}
|
||||
</div>
|
||||
|
||||
{/* JS 문제: 테스트별 채점 결과 */}
|
||||
{isJs && results && (
|
||||
{/* SQL 문제: 실행 결과 표 vs 기대 결과 표 — 눈으로 비교하며 고칠 수 있게 */}
|
||||
{isSql && sqlOutput && (
|
||||
<div className="card">
|
||||
<h2>
|
||||
실행 결과{' '}
|
||||
<span className={sqlOutput.passed ? 'ok' : 'ng'}>
|
||||
{sqlOutput.passed ? '통과' : '기대 결과와 다름'}
|
||||
</span>
|
||||
</h2>
|
||||
<SqlResultTable columns={sqlOutput.columns} rows={sqlOutput.rows} />
|
||||
{!sqlOutput.passed && sqlExpected && (
|
||||
<>
|
||||
<h3 style={{ marginTop: 16 }}>기대 결과</h3>
|
||||
<SqlResultTable columns={sqlExpected.expectedColumns} rows={sqlExpected.expectedRows} />
|
||||
{!sqlExpected.orderMatters && (
|
||||
<p className="muted" style={{ fontSize: 12.5 }}>
|
||||
이 문제는 행 순서는 채점하지 않아요 — 값이 같으면 통과입니다.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* JS 문제: 테스트별 채점 결과.
|
||||
SQL 문제도 재방문 시에는 저장된 요약이 이 형식이라 여기로 보여준다
|
||||
(다시 [실행]하면 위의 결과 표로 바뀐다). */}
|
||||
{(isJs || (isSql && !sqlOutput)) && results && (
|
||||
<div className="card">
|
||||
<h2>
|
||||
채점 결과{' '}
|
||||
@ -211,7 +329,7 @@ export default function CodingProblemPage() {
|
||||
)}
|
||||
|
||||
{/* 자바 문제: 자동채점이 없다는 걸 분명히 알려 준다 */}
|
||||
{!isJs && (
|
||||
{problem.language === 'JAVA' && (
|
||||
<div className="card">
|
||||
<h2>채점 안내</h2>
|
||||
<p>
|
||||
|
||||
@ -950,6 +950,40 @@ inline-code, .icode {
|
||||
background: color-mix(in srgb, var(--primary) 12%, transparent);
|
||||
color: var(--primary);
|
||||
}
|
||||
.lang-badge--sql {
|
||||
background: color-mix(in srgb, var(--teal) 12%, transparent);
|
||||
color: var(--teal);
|
||||
}
|
||||
|
||||
/* SQL 문제 — 실행 결과 표 */
|
||||
.sql-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.sql-table th,
|
||||
.sql-table td {
|
||||
border: 1px solid var(--line);
|
||||
padding: 6px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
.sql-table th {
|
||||
background: var(--surface-2, var(--primary-soft));
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* SQL 문제 — 준비 SQL(테이블+데이터) 보기 */
|
||||
.sql-setup {
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-2, var(--primary-soft));
|
||||
font-size: 12.5px;
|
||||
line-height: 1.6;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* 풀이 상태 배지 */
|
||||
.status-badge {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user