Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions API_SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# CodeBrainer API 명세서

## 문제 등록 API

### 엔드포인트
```
POST /internal/problems
```

### 설명
새로운 문제를 등록하거나 기존 문제를 수정합니다. (slug를 기준으로 upsert)

### Request Body
```json
{
"title": "두 수의 합",
"slug": "sum-of-two-numbers",
"tier": "BRONZE",
"level": 5,
"timeMs": 1000,
"memMb": 128,
"visibility": "PUBLIC",
"version": 1,
"categories": ["구현", "사칙연산"],
"languages": ["JAVA", "PYTHON", "JAVASCRIPT"],
"constraints": "1 ≤ A, B ≤ 1000",
"inputFormat": "첫째 줄에 두 정수 A와 B가 주어진다.",
"outputFormat": "A+B를 출력한다.",
"statement": "# 문제\n\n두 정수 A와 B를 입력받은 다음, A+B를 출력하는 프로그램을 작성하시오.\n\n## 입력\n\n첫째 줄에 A와 B가 주어진다. (0 < A, B < 10)\n\n## 출력\n\n첫째 줄에 A+B를 출력한다.",
"tests": [
{
"caseNo": 1,
"input": "1 2",
"output": "3",
"hidden": false,
"explanation": "1 + 2 = 3"
},
{
"caseNo": 2,
"input": "5 7",
"output": "12",
"hidden": false,
"explanation": "5 + 7 = 12"
},
{
"caseNo": 3,
"input": "100 200",
"output": "300",
"hidden": true,
"explanation": "숨겨진 테스트케이스"
}
],
"hints": [
{
"stage": 1,
"title": "첫 번째 힌트",
"contentMd": "입력을 받을 때는 Scanner나 BufferedReader를 사용하세요.",
"waitSeconds": 30
},
{
"stage": 2,
"title": "두 번째 힌트",
"contentMd": "두 수를 더한 결과를 출력하면 됩니다. `System.out.println(a + b);`",
"waitSeconds": 60
}
]
}
```

### Request Fields

| 필드 | 타입 | 필수 | 설명 | 예시 |
|------|------|------|------|------|
| `title` | String | ✅ | 문제 제목 | "두 수의 합" |
| `slug` | String | ✅ | 문제 URL용 고유 식별자 (영문, 숫자, 하이픈만) | "sum-of-two-numbers" |
| `tier` | String | ✅ | 난이도 티어 | "BRONZE", "SILVER", "GOLD", "PLATINUM", "DIAMOND" |
| `level` | Integer | ✅ | 티어 내 레벨 (1~5) | 5 |
| `timeMs` | Integer | ✅ | 시간 제한 (밀리초) | 1000 (1초) |
| `memMb` | Integer | ✅ | 메모리 제한 (MB) | 128 |
| `visibility` | String | ✅ | 공개 여부 | "PUBLIC", "PRIVATE" |
| `version` | Integer | ✅ | 문제 버전 | 1 |
| `categories` | String[] | ✅ | 문제 카테고리 목록 | ["구현", "사칙연산"] |
| `languages` | String[] | ✅ | 지원 언어 목록 | ["JAVA", "PYTHON", "JAVASCRIPT"] |
| `constraints` | String | ❌ | 제약 조건 | "1 ≤ A, B ≤ 1000" |
| `inputFormat` | String | ❌ | 입력 형식 설명 | "첫째 줄에 두 정수 A와 B가 주어진다." |
| `outputFormat` | String | ❌ | 출력 형식 설명 | "A+B를 출력한다." |
| `statement` | String | ✅ | 문제 설명 (Markdown) | "# 문제\n\n..." |
| `tests` | ProblemTestDto[] | ❌ | 테스트케이스 목록 | 아래 참조 |
| `hints` | ProblemHintDto[] | ❌ | 힌트 목록 | 아래 참조 |

#### ProblemTestDto

| 필드 | 타입 | 필수 | 설명 | 예시 |
|------|------|------|------|------|
| `caseNo` | Integer | ✅ | 테스트케이스 번호 | 1 |
| `input` | String | ✅ | 입력 데이터 | "1 2" |
| `output` | String | ✅ | 정답 출력 | "3" |
| `hidden` | Boolean | ❌ | 숨김 여부 (기본: false) | true |
| `explanation` | String | ❌ | 테스트케이스 설명 | "1 + 2 = 3" |

#### ProblemHintDto

| 필드 | 타입 | 필수 | 설명 | 예시 |
|------|------|------|------|------|
| `stage` | Short | ✅ | 힌트 단계 (순서) | 1 |
| `title` | String | ✅ | 힌트 제목 | "첫 번째 힌트" |
| `contentMd` | String | ✅ | 힌트 내용 (Markdown) | "Scanner를 사용하세요." |
| `waitSeconds` | Integer | ❌ | 힌트 오픈 대기 시간 (초) | 30 |

### Response
```json
{
"status": 200,
"body": 1
}
```

- `body`: 생성/수정된 문제의 ID (Long)

### 지원 언어 목록
```
JAVA
PYTHON
JAVASCRIPT
CPP
C
KOTLIN
GO
RUST
```

### 난이도 티어
```
BRONZE (브론즈)
SILVER (실버)
GOLD (골드)
PLATINUM (플래티넘)
DIAMOND (다이아몬드)
```

### 공개 여부
```
PUBLIC (공개)
PRIVATE (비공개)
```

### 예시 cURL 요청

```bash
curl -X POST http://localhost:8080/internal/problems \
-H "Content-Type: application/json" \
-d '{
"title": "두 수의 합",
"slug": "sum-of-two-numbers",
"tier": "BRONZE",
"level": 5,
"timeMs": 1000,
"memMb": 128,
"visibility": "PUBLIC",
"version": 1,
"categories": ["구현", "사칙연산"],
"languages": ["JAVA", "PYTHON", "JAVASCRIPT"],
"constraints": "1 ≤ A, B ≤ 1000",
"inputFormat": "첫째 줄에 두 정수 A와 B가 주어진다.",
"outputFormat": "A+B를 출력한다.",
"statement": "# 문제\n\n두 정수 A와 B를 입력받은 다음, A+B를 출력하는 프로그램을 작성하시오.\n\n## 입력\n\n첫째 줄에 A와 B가 주어진다. (0 < A, B < 10)\n\n## 출력\n\n첫째 줄에 A+B를 출력한다.",
"tests": [
{
"caseNo": 1,
"input": "1 2",
"output": "3",
"hidden": false,
"explanation": "1 + 2 = 3"
}
],
"hints": [
{
"stage": 1,
"title": "첫 번째 힌트",
"contentMd": "입력을 받을 때는 Scanner를 사용하세요.",
"waitSeconds": 30
}
]
}'
```

### 주의사항

1. **slug는 고유해야 합니다**
- 영문 소문자, 숫자, 하이픈(-)만 사용
- 예: `two-sum`, `binary-search-tree`

2. **같은 slug로 재요청하면 기존 문제를 덮어씁니다**
- version을 증가시켜 버전 관리 가능

3. **statement는 Markdown 형식**
- HTML 태그도 사용 가능
- 수식은 LaTeX 문법 지원

4. **tests는 최소 1개 이상 권장**
- hidden: true인 테스트케이스는 사용자에게 보이지 않음

5. **timeMs는 밀리초 단위**
- 1000 = 1초
- 2000 = 2초

6. **memMb는 메가바이트 단위**
- 128 = 128MB
- 256 = 256MB

---

## 기타 API

### 문제 목록 조회
```
GET /problems?page=0&size=20&sort=createdAt,desc
```

### 문제 상세 조회
```
GET /problems/{slug}
```

### 문제 제출
```
POST /submissions
```

더 자세한 API가 필요하면 말씀해주세요!

89 changes: 89 additions & 0 deletions DEBUG_STEPS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# 문제 목록이 안 보이는 문제 디버깅

## 1단계: 브라우저에서 API 응답 확인

1. 브라우저에서 `localhost:3000/problems` 페이지를 엽니다
2. `F12`를 눌러 개발자 도구를 엽니다
3. `Network` 탭으로 이동
4. 페이지를 새로고침 (`Ctrl + F5`)
5. `/api/problems` 또는 `problems` 요청을 찾아서 클릭
6. `Response` 탭에서 응답 확인

**예상되는 결과:**
```json
{
"problems": [
{
"id": "...",
"title": "Hashing",
"slug": "problem-15829",
"categories": ["해시"],
...
}
]
}
```

**만약 빈 배열이거나 에러가 나면:**
- 백엔드가 Supabase와 연결되지 않았거나
- Supabase에 데이터가 없는 것입니다

---

## 2단계: Supabase에서 데이터 직접 확인

Supabase SQL Editor에서 다음 쿼리 실행:

```sql
-- 1. 문제가 들어갔는지 확인
SELECT id, title, slug, categories, visibility
FROM problems
ORDER BY created_at DESC
LIMIT 10;

-- 2. 카테고리가 제대로 들어갔는지 확인
SELECT slug, title, categories
FROM problems
WHERE slug LIKE 'problem-%';
```

**예상 결과:**
- `categories`가 `["해시"]`, `["스택/큐"]`, `["힙"]` 등으로 **한국어**로 되어 있어야 함
- `visibility`가 `PUBLIC`이어야 함

---

## 3단계: 백엔드 로그 확인

백엔드 콘솔에서 에러 메시지를 확인합니다.

---

## 4단계: 백엔드 API 직접 호출

브라우저나 Postman에서:
```
http://localhost:8081/api/problems
```

이 URL로 직접 접속해서 응답을 확인합니다.

---

## 문제별 해결 방법

### 케이스 1: Supabase에 데이터가 없음
→ SQL을 Supabase SQL Editor에서 실행해야 합니다

### 케이스 2: categories가 영어로 되어 있음
→ UPDATE 문을 다시 실행해야 합니다

### 케이스 3: 백엔드가 Supabase에 연결되지 않음
→ application.yml의 DB 연결 정보 확인

### 케이스 4: 프론트엔드가 백엔드 API를 못 찾음
→ 백엔드가 8081 포트에서 실행 중인지 확인




Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.codebrainer.orchestrator.controller;

import com.codebrainer.orchestrator.dto.AdminDashboardStats;
import com.codebrainer.orchestrator.dto.PageResponse;
import com.codebrainer.orchestrator.dto.UserListItem;
import com.codebrainer.orchestrator.service.AdminService;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
* 관리자 컨트롤러
*/
@RestController
@RequestMapping("/api/admin")
@Validated
public class AdminController {

private final AdminService adminService;

public AdminController(AdminService adminService) {
this.adminService = adminService;
}

/**
* 관리자 대시보드 통계 조회
*/
@GetMapping("/dashboard/stats")
public ResponseEntity<AdminDashboardStats> getDashboardStats() {
AdminDashboardStats stats = adminService.getDashboardStats();
return ResponseEntity.ok(stats);
}

/**
* 사용자 목록 조회
*/
@GetMapping("/users")
public ResponseEntity<PageResponse<UserListItem>> getUserList(
@RequestParam(value = "page", required = false, defaultValue = "0") @Min(0) Integer page,
@RequestParam(value = "size", required = false, defaultValue = "20") @Min(1) @Max(100) Integer size,
@RequestParam(value = "sort", required = false, defaultValue = "createdAt,desc") String sort
) {
String[] sortParams = sort.split(",");
String property = sortParams[0];
Sort.Direction direction = sortParams.length > 1 && sortParams[1].equalsIgnoreCase("asc")
? Sort.Direction.ASC
: Sort.Direction.DESC;

Pageable pageable = PageRequest.of(page, size, Sort.by(direction, property));
Page<UserListItem> result = adminService.getUserList(pageable);

PageResponse<UserListItem> response = new PageResponse<>(
result.getContent(),
result.getNumber(),
result.getSize(),
result.getTotalElements(),
result.getTotalPages()
);

return ResponseEntity.ok(response);
}
}

Loading