Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ body:
label: 재현 방법
description: 버그를 재현할 수 있는 단계를 작성해주세요.
placeholder: |
1. DM으로 `!참가신청` 입력
2. 백준 핸들 입력
1. `/contest list` 입력
2. 특정 대회 ID 선택
3. ...
validations:
required: true
Expand Down
6 changes: 3 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

## 체크리스트

- [ ] 기존 테스트가 통과합니다 (`pytest`)
- [ ] 새로운 기능에 대한 테스트를 추가했습니다
- [ ] 하드코딩된 값 없이 `config.py` 상수를 사용했습니다
- [ ] 빌드 및 타입 체크가 통과합니다 (`npm run type-check`)
- [ ] 새로운 기능에 대한 테스트를 추가했습니다 (해당하는 경우)
- [ ] 하드코딩된 값 없이 `src/config.ts` 및 환경 변수를 사용했습니다
- [ ] 코드에 민감한 정보(토큰, 키 등)가 포함되어 있지 않습니다
161 changes: 55 additions & 106 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,151 +1,101 @@
# Copilot 코드 리뷰 지침
# Copilot 코드 리뷰 지침 (TypeScript/Cloudflare Workers)

> 한글로 리뷰해주세요

---

## 기술 스택

- **언어:** Python 3.11+
- **Discord:** `discord.py` 2.x (Cog 아키텍처)
- **DB:** SQLite + `SQLAlchemy` 2.x (async)
- **HTTP:** `httpx` (async) — solved.ac API 통신
- **Google Sheets:** `gspread` + 서비스 계정
- **스케줄러:** `APScheduler` 3.x
- **설정 관리:** `pydantic-settings` + `.env`
- **테스트:** `pytest` + `pytest-asyncio` + `respx`
- **기타:** `tenacity` (재시도), `python-dotenv`
- **언어:** TypeScript 5.7+ (Node.js/Workers 환경)
- **런타임:** Cloudflare Workers
- **프레임워크:** [Hono](https://hono.dev/)
- **DB:** Cloudflare D1 (SQLite 호환)
- **Discord:** Interaction API (Slash Commands), Ed25519 서명 검증
- **HTTP:** Fetch API (Native) — solved.ac API 통신 등
- **설정 관리:** Cloudflare Workers `Env` 바인딩 + `src/config.ts` (정제된 Config)
- **빌드/배포:** Wrangler (Cloudflare CLI)
- **타입 체크:** `tsc` (TypeScript Compiler)

---

## 네이밍 컨벤션

| 대상 | 규칙 | 예시 |
| ----------------- | ------------------ | ---------------------- |
| 모듈/패키지명 | `snake_case` | `solved_ac.py` |
| 클래스명 | `PascalCase` | `ScoringStrategy` |
| 함수/변수명 | `snake_case` | `get_user_info` |
| 파일/디렉토리명 | `camelCase` | `adminService.ts` |
| 클래스명 | `PascalCase` | `ScoringService` |
| 함수/변수명 | `camelCase` | `getUserInfo` |
| 상수명 | `UPPER_SNAKE_CASE` | `SOLVEDAC_BASE_URL` |
| Enum 멤버 | `UPPER_SNAKE_CASE` | `League.ROOKIE` |
| 비공개 멤버 | `_leading_underscore` | `_session_cache` |
| Cog 클래스 | `PascalCase + Cog` | `RegistrationCog` |
| Interface/Type | `PascalCase` | `AppVariables` |
| Enum 멤버 | `PascalCase` | `League.Rookie` |

---

## 프로젝트 구조

```
kkemi/
├── bot/
│ ├── main.py # 봇 진입점, 초기화
│ ├── config.py # pydantic-settings 기반 전체 설정
│ ├── app_context.py # 서비스 의존성 컨텍스트
│ ├── database/
│ │ ├── models.py # SQLAlchemy ORM 모델
│ │ └── session.py # 비동기 세션 팩토리
│ ├── cogs/
│ │ ├── registration.py # 참가자 DM 등록 흐름
│ │ ├── admin.py # 관리자 DM 명령어
│ │ └── leaderboard.py # 리더보드 게시 및 스케줄러
│ ├── services/
│ │ ├── solved_ac.py # solved.ac API 클라이언트
│ │ ├── google_sheets.py # gspread 자격 확인 및 저장
│ │ ├── registration.py # 등록 비즈니스 로직
│ │ ├── scoring.py # 리그별 점수 계산 (Strategy 패턴)
│ │ ├── league.py # 리그 분류 로직
│ │ └── contest_query.py # 대회 공통 쿼리
│ ├── domain/
│ │ └── admin_log.py # 관리자 액션 로그
│ └── utils/
│ ├── embeds.py # Discord 임베드 빌더
│ └── decorators.py # 관리자 전용·DM 전용 가드
tests/
├── test_solved_ac.py
├── test_registration_cog.py
├── test_scoring.py
└── test_admin_cog.py
src/
├── index.ts # Worker 진입점, Hono 앱 초기화
├── config.ts # Env 바인딩 정제 및 설정 객체 생성
├── context.ts # AppContext (의존성 주입 컨테이너)
├── types.ts # 공통 타입 정의
├── errors.ts # AppError 및 커스텀 예외
├── db/ # 영속성 레이어 (D1 Repository)
│ ├── contestRepository.ts
│ └── participantRepository.ts
├── discord/ # Discord Interaction 레이어
│ ├── interactions.ts # 디스패처
│ ├── helpers.ts # 임베드/응답 헬퍼
│ └── commands/ # 커맨드 핸들러
├── domain/ # 도메인 모델 (Entities)
├── middleware/ # Hono 미들웨어 (Auth, Context 주입)
├── routes/ # Hono 라우트 (Controller 레이어)
└── services/ # 비즈니스 로직 레이어
├── solvedAcService.ts
└── registrationService.ts
```

---

## 아키텍처 원칙

### 계층 분리
### 계층 분리 (Layered Architecture)

- **Cog (표현 계층):** Discord 메시지 수신/응답만 담당. 비즈니스 로직을 직접 구현하지 않는다.
- **Service (비즈니스 계층):** 핵심 로직. 외부 API 호출, DB 쿼리, 점수 계산 등을 수행한다.
- **Database (영속 계층):** ORM 모델과 세션 관리만 담당한다.
- **Route/Discord (표현 계층):** HTTP 요청이나 Discord Interaction 수신. 비즈니스 로직을 직접 구현하지 않고 Service를 호출함.
- **Service (비즈니스 계층):** 핵심 비즈니스 로직. 여러 Repository를 조합하거나 외부 API를 호출하여 도메인 규칙을 처리함.
- **Repository (영속 계층):** D1 데이터베이스(SQL) 접근 담당. SQL 쿼리를 Service 레이어와 분리함.

### 의존성 방향
### 의존성 주입 (Dependency Injection)

`Cog → Service → Database / 외부 API`

Cog에서 직접 SQLAlchemy 세션을 열거나 httpx를 호출하지 않는다. 반드시 Service를 통해 접근한다.
- `AppContext`를 통해 모든 서비스와 레포지토리를 관리함.
- Hono 미들웨어에서 `c.set('ctx', context)`를 통해 핸들러로 전달함.

---

## 설정 관리

- 모든 설정값은 `config.py`에서 `pydantic-settings`로 관리한다
- **코드 내 매직 스트링/매직 넘버를 사용하지 않는다** — 항상 `config`에서 임포트한다
- 환경변수는 `.env` 파일에서 로드한다
- 봇 명령어 키워드도 config 상수로 정의해 로직 수정 없이 변경 가능하게 한다

---

## 비동기 패턴

- DB 세션은 `async with`로 관리하고, 세션 누수를 방지한다
- 외부 API 호출(`httpx`, `gspread`)은 반드시 `try/except`로 감싸고, `tenacity`로 재시도한다
- `asyncio.gather` 사용 시 개별 태스크 실패가 전체를 중단시키지 않도록 주의한다

---

## 에러 처리

- 서비스 레이어에서 발생하는 비즈니스 예외는 **한국어 메시지**를 포함해야 한다
- Cog에서는 예외 메시지를 Discord 임베드로 그대로 전달한다 — 내부 스택 트레이스를 노출하지 않는다
- 외부 API 호출은 원시 HTTP 에러 대신 타입이 지정된 예외(`SolvedACError`, `UserNotFoundError` 등)로 변환한다
- 처리되지 않은 예외로 봇이 중단되어서는 안 된다

---

## 로깅

- Python 표준 `logging` 모듈 사용
- 포맷: `[타임스탬프] [레벨] [cog/서비스] 메시지`
- stdout + 회전 파일 `logs/bot.log` 동시 출력
- `print()` 대신 반드시 `logger`를 사용한다

---

## 테스트

- 비동기 테스트: `pytest-asyncio` (`asyncio_mode = auto`)
- HTTP 모킹: `respx` (httpx 응답 모킹)
- Discord 모킹: `unittest.mock.AsyncMock`
- `services/` 디렉터리 코드 커버리지 **80% 이상** 목표
- 테스트 파일은 `tests/` 디렉터리에 `test_*.py` 패턴으로 작성한다
- 모든 환경 변수는 `wrangler.toml` 또는 `wrangler secret`을 통해 주입됨.
- `src/config.ts`의 `buildConfig(env)`를 통해 원시 `Env`를 타입 안정성이 보장된 `Config` 객체로 변환하여 사용함.
- **코드 내 하드코딩된 상수 사용 금지** — 반드시 `config`를 통해 접근함.

---

## 코드 품질
## 비동기 및 에러 처리

- 불필요한 `print()`나 `console.log` 대신 `logging` 사용 여부를 확인해주세요
- 사용하지 않는 import가 있는지 확인해주세요
- 타입 힌트가 누락된 함수 시그니처가 있는지 확인해주세요
- 하드코딩된 값이 `config.py` 상수로 분리되어야 하는지 확인해주세요
- `async def`인데 `await`가 없는 함수는 동기 함수로 변경해야 하는지 확인해주세요
- DB 세션이 `async with`로 적절히 관리되는지 확인해주세요
- Cloudflare Workers는 `fetch` 기반의 비동기 환경임.
- 비즈니스 예외는 `AppError`를 상속받아 구현하며, **한국어 메시지**를 포함함.
- Discord 핸들러에서는 예외 발생 시 `msg('❌ 에러 메시지')` 형태로 사용자에게 응답함.
- 외부 API 호출(`fetch`) 시 적절한 타임아웃과 에러 핸들링을 적용함.

---

## 보안
## 코드 품질 가이드

- API 키나 토큰이 코드에 하드코딩되어 있는지 확인해주세요
- `.env` 파일이 `.gitignore`에 포함되어 있는지 확인해주세요
- Discord 관리자 명령어가 `ADMIN_USER_IDS` 검증을 거치는지 확인해주세요
- 사용자 입력값(BOJ 핸들, 실명 등)에 대한 기본 검증이 있는지 확인해주세요
- `console.log` 보다는 체계적인 로깅(필요 시)을 고려하되, Workers의 로그 제한을 인지할 것.
- 타입 힌트를 엄격하게 사용하며, `any` 사용을 지양함.
- 비즈니스 로직은 반드시 `services/` 레이어에 위치해야 함.
- D1 쿼리 작성 시 SQL Injection 방지를 위해 반드시 바인딩 매개변수(`?`)를 사용함.
- 불필요한 의존성을 피하고, Workers의 번들 크기 제한을 고려함.

---

Expand All @@ -157,7 +107,6 @@ Cog에서 직접 SQLAlchemy 세션을 열거나 httpx를 호출하지 않는다.
feat: 새로운 기능
fix: 버그 수정
refactor: 리팩토링
test: 테스트 추가/수정
chore: 기타 작업 (CI, 설정 등)
docs: 문서 수정
```
11 changes: 5 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
node-version: '20'
cache: npm
bun-version: latest

- name: Install dependencies
run: npm ci
run: bun install --frozen-lockfile

- name: Type check
run: npm run type-check
run: bun run type-check
42 changes: 20 additions & 22 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
# Python
__pycache__/
**/__pycache__/
*.py[cod]
*$py.class
*.pyo
*.pyd
# Node.js
node_modules/
dist/
build/
.npm
package-lock.json

# Virtual envs
.venv/
venv/
env/
# Cloudflare Workers
.wrangler/
.dev.vars
.wrangler-account.json

# Tooling
.pytest_cache/
.coverage
.coverage.*
htmlcov/
.mypy_cache/
.ruff_cache/
.tox/
.nox/
# TypeScript
*.tsbuildinfo

# Runtime
# Runtime / Secrets
.env
*.env
.env.*
Expand All @@ -40,4 +32,10 @@ logs/
# OS
.DS_Store
Thumbs.db
/node_modules

# Python leftovers (if any)
__pycache__/
*.py[cod]
.venv/
.pytest_cache/
.mypy_cache/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,4 @@ npm run dev # 로컬 개발 서버
npm run type-check # 타입 체크
npm run deploy # 배포
```

Loading
Loading