Compare commits

...

2 Commits

26 changed files with 820 additions and 557 deletions
+14 -9
View File
@@ -166,6 +166,7 @@ ORDER BY A.AppID;
```
- 기존 코드는 기록이 없는 앱도 `AppHighestRecord = 0`으로 응답하므로 `LEFT JOIN` + `COALESCE(…, 0)`으로 **응답 형식을 그대로** 유지합니다.
- ⚠️ (2026-09-15 검토) `HighestRecord``FLOAT``COALESCE`를 쓰면 결과가 `DOUBLE`로 바뀌어 JSON 소수점이 길어질 수 있습니다. 실제 구현은 `MAX(...) GROUP BY`로 조회하고 0은 PHP에서 채웠습니다.
- `get_typing_practice_app_list()`와 조건(`AppType = ? AND Status = 1`)이 같으므로 두 조회를 합쳐 앱 이름까지 한 번에 가져올 수도 있습니다.
- 안1의 UNIQUE 키 `(MaestroID, PlayerID, AppID)`가 있으면 `LEFT JOIN` 한 건당 인덱스 1회 탐색입니다.
@@ -274,8 +275,8 @@ function build_period_condition($column, $period, $date, $time) {
| 파일 | 내용 | 수정 |
|---|---|---|
| [typing_exam_collection.php](../../../src/web/php/db/typing_exam_collection.php) `getHighestRecordArrayForAllWriting()` | `bind_param("iii", …)`에 값 2개 | 호출처가 있으면 `"ii"`, 없으면 함수 삭제 |
| `server/record/*.php` 여러 파일 | `if($replyJSON.length === 0)` (PHP에서는 항상 거짓) | `count(...) === 0`으로 수정하거나 불필요하면 삭제 |
| [typing_exam_collection.php](../../../src/web/php/db/typing_exam_collection.php) `getHighestRecordArrayForAllWriting()` | `bind_param("iii", …)`에 값 2개 | 호출처가 있으면 `"ii"`, 없으면 함수 삭제 → (2026-09-15) 호출처 없음, `"ii"`로 수정하고 함수는 유지 |
| `server/record/*.php` 여러 파일 | `if($replyJSON.length === 0)` (PHP에서는 항상 거짓) | **if 블록 삭제**. `count(...) === 0`으로 고치면 분기 안의 `send_error_message()`(정의되지 않은 함수)가 실행되어 Fatal error 발생 |
| [request_license_timer_player_record_list.php](../../../src/web/server/record/request_license_timer_player_record_list.php) | 종료일 당일 기록 누락, 존재하지 않는 `LS.AppID` 참조 | 3-1 패턴으로 재작성 |
---
@@ -342,11 +343,15 @@ function build_period_condition($column, $period, $date, $time) {
## 8. 체크리스트
- [ ] 기록 목록 API 3종: 바인딩 전환, COUNT 제거, 상한, 종료일 통일
- [ ] 기록 목록 화면: 상한 안내 / 페이지
- [x] **기록 목록 API 3종: 바인딩 전환, COUNT 유지, LIMIT 상한 없이, 종료일 통일** (2026-09-15 완료)
- request_app_player_record_list.php ✅
- request_writing_player_record_list.php ✅
- request_license_timer_player_record_list.php ✅
- [ ] 기록 목록 화면: 상한 안내 / 페이지 (생략)
- [ ] 변경 전후 검색 결과 건수 비교, SQL Injection 입력 테스트
- [ ] 랭킹 기간 조건 공용 함수 (안1 작업과 함께)
- [ ] 메뉴 N+1 제거 (연습·테스트, `menu_collection.php`, `writing_collection.php`)
- [ ] 작은 버그 3종
- [ ] (측정 후 필요 시) 랭킹 캐시 + 저장 시 무효화
- [ ] `ranking` 테이블 처리 결정 (캐시 재활용 또는 삭제)
- [ ] ~~랭킹 기간 조건 공용 함수 (안1 작업과 함께)~~**보류** (2026-09-15): 날짜 조건은 커밋 629c8ff에서 12곳 모두 인덱스 적용 형태로 이미 수정됨. 회귀 위험 대비 이득이 작음
- [x] **메뉴 N+1 제거 — 새 메뉴 경로** (2026-09-15 코드 완료): `menu_collection.php`, `writing_collection.php`, `menu_list.php`
- [ ] 메뉴 N+1 제거 — 옛 메뉴 `server/app/menu_active_typing_*_app_list.php` (범위 제외, 테스트 계정 경로에서만 사용)
- [x] **작은 버그** (2026-09-15 코드 완료): `.length` if 블록 삭제 13개 파일(추가 발견 4개 포함), `history_record.php` 미정의 변수 push 삭제, `getHighestRecordArrayForAllWriting()` `"ii"` 수정(함수 유지). 라이선스 타이머 `AppID` 참조는 유지
- [ ] (측정 후 필요 시) 랭킹 캐시 + 저장 시 무효화 — **이번 작업에서 제외** (2026-09-15, 적용 후 측정해서 재검토)
- [ ] `ranking` 테이블 처리 결정 (캐시 재활용 또는 삭제) — 랭킹 캐시와 함께 보류
@@ -63,23 +63,24 @@ WHERE MaestroID = 181
AND RecordDateTime >= DATE_FORMAT(NOW(), '%Y-%m-%d %H:00:00')
AND RecordDateTime < DATE_FORMAT(NOW(), '%Y-%m-%d %H:00:00') + INTERVAL 1 HOUR;
-- 개선 쿼리 2: 함수 제거 (빠름)
-- 개선 쿼리 2: 함수 제거 (빠름) - 일일 랭킹
SELECT '=== Query 2: Daily ranking ===' AS msg;
SELECT PlayerID, COUNT(*) FROM best_record
SELECT PlayerID, MAX(BestRecord) FROM best_record
WHERE MaestroID = 181
AND AppID = 21
AND RecordDateTime >= DATE_FORMAT(NOW(), '%Y-%m-%d %H:00:00')
AND RecordDateTime < DATE_FORMAT(NOW(), '%Y-%m-%d %H:00:00') + INTERVAL 1 HOUR
AND RecordDateTime >= CURDATE()
AND RecordDateTime < CURDATE() + INTERVAL 1 DAY
GROUP BY PlayerID
ORDER BY COUNT(*) DESC
ORDER BY MAX(BestRecord) DESC
LIMIT 10;
-- 개선 쿼리 3: 월간 (함수 제거)
-- 개선 쿼리 3: 월간 (함수 제거) - 월간 랭킹
SELECT '=== Query 3: Monthly ranking ===' AS msg;
SELECT PlayerID, MAX(BestRecord) FROM best_record
WHERE MaestroID = 181
AND RecordDateTime >= DATE_FORMAT(NOW(), '%Y-%m-01')
AND RecordDateTime < DATE_FORMAT(DATE_ADD(NOW(), INTERVAL 1 MONTH), '%Y-%m-01')
AND AppID = 21
AND RecordDateTime >= CAST(DATE_FORMAT(CURDATE(), '%Y-%m-01') AS DATE)
AND RecordDateTime < CAST(DATE_FORMAT(CURDATE(), '%Y-%m-01') AS DATE) + INTERVAL 1 MONTH
GROUP BY PlayerID
ORDER BY MAX(BestRecord) DESC
LIMIT 10;
@@ -0,0 +1,331 @@
# 안5. 애플리케이션(PHP/화면) 개선 실행 계획
> 참고: [`doc/plan/db/06-option5-application-layer.md`](../06-option5-application-layer.md) | 단계: **병행** | 난이도: 하~중 | 위험도: 낮음 | 예상 작업량: 2~4일
---
## 📋 요약
DB 구조는 그대로 두고 PHP 코드의 비효율과 보안 이슈를 고친다:
| 항목 | 개선 내용 | 파일 수 | 영향 범위 |
|------|---------|--------|---------|
| **3-1. 기록 목록 API** | SQL Injection 제거 + COUNT 제거 + 상한 추가 | 3개 + 화면 1개 | 관리자 화면 |
| **3-2. 메뉴 N+1 쿼리** | 앱마다 조회 → 종류별 1회 조회 | 4개 | 학생 메뉴 |
| **3-4. 랭킹 코드 정리** | 중복 함수 → 공용 함수 1개 (**보류**) | 5개 파일 (12개 함수) | 랭킹 화면 |
| **3-3. 랭킹 캐시** | 선택 (측정 후 필요시) | 2개 + DB | 동시 접속 시 |
| **3-5. 작은 버그** | 각 파일 수정 중 처리 | 3개 파일 | 부분 |
---
## 🔄 실행 순서
### 1단계: 기록 목록 API 보안 + 성능 (우선순위 높음)
**소요: ~3시간 | 위험도: 낮음 | 롤백: git revert만 가능**
#### 대상 파일
```
src/web/server/record/
├── request_app_player_record_list.php
├── request_writing_player_record_list.php
└── request_license_timer_player_record_list.php
src/web/module/
└── maestro_section_record.html (화면 안내)
```
#### 변경 내용
- ✅ SQL 문자열 연결 → PreparedStatement + bind_param()
-`get_player_record_total_count()` 호출 제거 (화면에서 미사용)
- ✅ "전체 보기" 상한 도입 (예: MAX 1,000건)
- ✅ 종료일 처리 통일 (`< DATE(?) + INTERVAL 1 DAY`)
- ✅ 자격증 목록 `AppID` 참조 수정
#### 테스트 항목
```
□ 과목 전체/묶음(1000~1003)/개별 앱 검색
□ 이름 검색 있음/없음, 기간 1일/1개월
□ "전체 보기" → 상한 안내 표시
□ 종료일 당일 기록 포함 여부
□ 보안: 이름 칸에 ' OR '1'='1 입력 → 결과 비거나 해당 검색만
□ 자격증 목록 정상 조회
```
#### 체크리스트
- [x] 3개 파일 bind_param 변환 완료
- [x] COUNT 유지 (요청사항)
- [x] LIMIT 상한 없이 유지 (요청사항)
- [x] 종료일 < DATE(?) + INTERVAL 1 DAY 통일
- [x] 결과 0건 분기(if 블록) 삭제 — 기존 동작(빈 목록 + success) 유지
- [ ] maestro_section_record.html 상한 안내 추가 (생략)
- [ ] 스테이징 테스트 완료
- [ ] SQL Injection 테스트 통과
- [ ] 전후 검색 결과 건수 비교 통과
---
### 2단계: 랭킹 코드 정리 — ⏸️ 보류 (2026-09-15)
**소요: ~2시간 | 위험도: 낮음 | 이유: 안1 쿼리 조건 변경 시 공용 함수 1곳만 수정**
> **보류 사유 (검토 결과)**
> - 날짜 조건은 커밋 `629c8ff`에서 12곳 모두 인덱스 적용 형태로 이미 수정됨 → 원래 목적 달성
> - 실제 대상은 5개 파일·12개 함수이고, 기간 기준이 서버 시각(`app_ranking.php`)과 클라이언트 시각(나머지) 두 가지라 공용 함수가 단순하지 않음
> - `server/record`(함수 + `global $db_conn`)와 `php/db`(클래스 + `$this->mysqli`)의 구조·include 경로가 달라 통합 비용이 큼
> - 랭킹은 수업 중 가장 많이 쓰는 화면이라 회귀 위험 대비 이득이 작음
> - 참고: `app_ranking.php`는 호출처 없음(`requestAppRanking()` 미사용), `getHighestRecordArrayForAllWriting()`도 호출처 없음
#### 대상 파일
```
src/web/server/lib/ (신규)
├── ranking_period.php ← 새로 생성 (기간 조건 공용 함수)
src/web/server/record/
├── app_ranking.php (get_ranking_hour/day/month)
├── ranking_record_hour.php
├── ranking_record_day.php
└── ranking_record_month.php
src/web/php/db/
└── typing_exam_collection.php (getRankingRecord*, getRankingMinusRecord*)
```
#### 변경 내용
- ✅ 공용 함수 `build_period_condition($column, $period, $date, $time)` 생성
- 반환: (SQL 조각, 타입 문자열, 바인딩 값 배열)
- 사용자 입력 금지: `$column`은 코드 고정값만
- ✅ 기존 엔드포인트 응답 형식 그대로 유지 (클라이언트 변경 없음)
- ✅ 10개 파일의 중복 쿼리 → 공용 함수 호출로 통합
#### 체크리스트
- [ ] `src/web/server/lib/ranking_period.php` 생성
- [ ] app_ranking.php 내부 구현만 변경
- [ ] ranking_record_*.php 내부 구현만 변경
- [ ] typing_exam_collection.php 내부 구현만 변경
- [ ] 스테이징 랭킹 조회 테스트
- [ ] 게임 클라이언트 변경 불필요 확인
---
### 3단계: 메뉴 N+1 쿼리 제거 — ✅ 코드 완료 (2026-09-15, 새 메뉴 경로만)
**소요: ~2시간 | 위험도: 낮음 | 영향: 메뉴 진입 속도 (4 + 앱 수 → 약 6회 쿼리)**
> **검토 결과 및 결정**
> - 사용 중인 메뉴는 `client/main_menu.html` → `php/menu/menu_list.php` → `menu_collection.php` / `writing_collection.php` 경로 (탭 클릭마다 호출)
> - 옛 메뉴 `server/app/menu_active_typing_*_app_list.php`는 `start.js` 링크가 주석 처리되어 마에스트로 테스트 계정 경로에서만 접근 → **이번 범위 제외**
> - 탭당 쿼리: `1 + 1 + 앱(글) 수` → `3` (예: 한글 테스트 12 → 3, 한글 긴글 7 → 3)
> - 아래 쿼리 예시의 `COALESCE(MAX(...), 0)`은 **사용하지 않음**: `FLOAT`가 `DOUBLE`로 바뀌어 JSON 소수점이 길어질 수 있음 → `MAX(...) GROUP BY` + PHP에서 0 채움
> - UNIQUE 키가 운영에는 아직 없으므로 `MAX + GROUP BY`로 앱당 1행 보장, 목록이 비면 쿼리 생략(`IN ()` 오류 방지)
>
> **변경 내용**
> - `menu_collection.php`: `getTypingHighestRecordList()`를 `AppID IN (...)` 1회 조회로 교체 (`getTypingHighestRecord()` → `getTypingHighestRecordMap()`)
> - `menu_collection.php`: `getWritingHighestRecordList()`를 1회 조회로 교체
> - `writing_collection.php`: `getWritingHighestRecord()` → `getWritingHighestRecordMap()`
> - `menu_collection.php`: `getHighestRecordList()`에 `$appGroup` 인자 추가 (미정의 변수 버그 수정), `menu_list.php` 호출부 수정
> - `menu_collection.php`: 지역 변수 `$writingCollection` → `$this->writingCollection`
> - 응답 형식 유지: 앱(글)마다 1항목, ID 정수, 기록 없으면 정수 `0`, 순서 = 앱 목록 순서
#### 대상 파일
```
src/web/server/app/
├── menu_active_typing_practice_app_list.php
└── menu_active_typing_test_app_list.php
src/web/php/db/
├── menu_collection.php (getTypingHighestRecordList)
└── writing_collection.php (getWritingHighestRecord)
```
#### 변경 내용
- ✅ 앱마다 조회 (N+1) → 종류별 1회 + LEFT JOIN 통합
- ✅ 기존 응답 형식 그대로 (기록 없는 앱도 AppHighestRecord=0)
- ✅ COALESCE(..., 0) 사용해 NULL → 0 변환
#### 쿼리 변경 예시
```php
// Before: 앱 수만큼 반복
for ($i = 0; $i < $count; $i++) {
SELECT MAX(AHR.HighestRecord)
FROM app A
INNER JOIN app_highest_record AHR ...
WHERE A.AppID = ?
}
// After: 1회 조회
SELECT A.AppID, COALESCE(MAX(AHR.HighestRecord), 0) AS AppHighestRecord
FROM app A
LEFT JOIN app_highest_record AHR
ON AHR.AppID = A.AppID AND AHR.MaestroID = ? AND AHR.PlayerID = ?
WHERE A.AppType = ? AND A.Status = 1
GROUP BY A.AppID
ORDER BY A.AppID;
```
#### 테스트 항목
```
□ 새 메뉴(main_menu.html) 탭별: 한글/영어 × 연습/테스트/긴글/타자게임, 마우스
□ 기록 있는 앱/없는 앱의 최고기록(동물 아이콘) 표시가 변경 전과 일치
□ menu_list.php 응답 JSON의 highestRecordList를 변경 전후 비교 (소수점 자릿수 포함)
□ 기록이 전혀 없는 학생 계정으로 메뉴 진입 (오류 없이 모두 0)
□ PHP 에러 로그에 Warning/Notice 없음
```
#### 체크리스트
- [ ] menu_active_typing_practice_app_list.php 수정 (범위 제외: 옛 메뉴)
- [ ] menu_active_typing_test_app_list.php 수정 (범위 제외: 옛 메뉴)
- [x] menu_collection.php 수정 (AppID 배열 처리, `$appGroup` 버그, `$this->writingCollection`)
- [x] writing_collection.php 수정 (`getWritingHighestRecordMap()`)
- [x] menu_list.php 호출부 수정
- [ ] 스테이징 메뉴 진입 테스트
- [ ] 기록 있는/없는 앱 표시 일치 확인
---
### 4단계: 작은 버그 수정 — ✅ 코드 완료 (2026-09-15)
**소요: ~30분 | 위험도: 매우 낮음**
> **검토 결과 및 결정**
> - PHP 7.3에서 `$x.length === 0`은 `$x . "length"` → `"Arraylength" === 0` → **항상 거짓**. 분기는 한 번도 실행된 적 없고 요청마다 에러 로그에 Warning만 남음. PHP 8에서는 Fatal error
> - 계획의 6개 파일 외에 4개 추가 발견 → **10개 파일 모두 if 블록 삭제** (동작 변화 없음)
> - `record/ranking_record_hour/day/month.php`, `record/history_record.php`, `record/request_app_highest_record.php`, `app/how_to_play.php`
> - (추가) `app/menu_active_app_list.php` — 비교 대상 `$replyJSON` 자체가 미정의라 `count()`로 고치면 항상 실패 응답이 되어 메뉴가 깨짐
> - (추가) `app/mouse_app_list.php`, `app/typing_app_list.php`, `player/search_player_list.php` — `search_player_list`는 검색 결과 0명일 때 지금처럼 빈 목록 표시 유지
> - `history_record.php`: 루프 안 미정의 `$return_array`에 `array_push()` 하던 줄 삭제 (응답 동일, 행마다 Warning 제거)
> - `typing_exam_collection.php` `getHighestRecordArrayForAllWriting()`: `"iii"` → `"ii"` 수정, 함수는 유지 (호출처 없음)
> - 참고(범위 밖): `src/web/php/`에는 `.htaccess`가 없고 스테이징 Dockerfile에 `php.ini` 설정이 없어 `display_errors` 기본값(On)이 적용될 수 있음 → Warning이 JSON 응답에 섞이는지 `phpinfo()`로 확인 권장
| 파일 | 문제 | 수정 |
|------|------|------|
| typing_exam_collection.php | bind_param("iii", …)에 값 2개만 | "ii"로 수정 또는 함수 삭제 |
| server/record/*.php 여러 파일 | `if($replyJSON.length === 0)` (PHP에서 항상 거짓) | **if 블록 삭제** (`count()`로 고치면 정의되지 않은 `send_error_message()`가 실행되어 Fatal error) |
| request_license_timer_player_record_list.php | 종료일 당일 기록 누락, 존재하지 않는 LS.AppID | 종료일은 1단계에서 수정 완료. AppID 참조는 화면이 3000만 보내므로 유지 (추후 정리) |
#### 체크리스트
- [x] typing_exam_collection.php bind_param `"iii"``"ii"` (함수 유지)
- [x] request_*_player_record_list.php 3개: if 블록 삭제 (1단계에서 처리)
- [x] 남은 파일 if 블록 삭제: ranking_record_hour/day/month.php, history_record.php, request_app_highest_record.php, app/how_to_play.php
- [x] 추가 발견 파일 if 블록 삭제: app/menu_active_app_list.php, app/mouse_app_list.php, app/typing_app_list.php, player/search_player_list.php
- [x] history_record.php 미정의 `$return_array` push 줄 삭제
- [ ] 스테이징 테스트: 결과·랭킹 화면, 시작 화면(히스토리·최고기록·게임 방법), 메뉴, 마에스트로 앱 활성화·기록·학생 검색(0명 포함)
- [ ] 스테이징 PHP 에러 로그에서 `Use of undefined constant length` Warning 사라졌는지 확인
---
### 5단계: 랭킹 캐시 — ⏸️ 이번 작업에서 제외 (2026-09-15)
> 1·3·4단계 스테이징/운영 적용 후 수업 시간대 랭킹 쿼리 부하를 측정해서 필요할 때 다시 검토합니다. 진행 시 2단계 보류로 랭킹 코드가 5개 파일·12개 함수에 흩어져 있다는 점을 고려해 캐시 적용 위치부터 재검토해야 합니다.
**소요: ~2시간 | 위험도: 낮음 | 조건: 1·2 완료 후 측정에서 필요 판단**
#### 배경
- 교실 수업: 학생 30명 동시 → 동일한 랭킹 쿼리 90회 실행
- 랭킹 화면 1시간 열어 둠 → 기록 변화 없어도 720회 쿼리 (5초마다)
#### 방법 (DB 캐시 테이블 권장)
```sql
-- ranking 테이블 백업 후 삭제
CREATE TABLE ranking_cache (
MaestroID INT UNSIGNED NOT NULL,
AppID INT UNSIGNED NOT NULL,
RankingType CHAR(10) NOT NULL,
CachedDateTime DATETIME NOT NULL,
Payload MEDIUMTEXT NOT NULL,
PRIMARY KEY (MaestroID, AppID, RankingType)
);
```
#### 체크리스트
- [ ] 3-1·3-2 완료 후 성능 측정
- [ ] 랭킹 쿼리 슬로우 로그 확인 (필요시만 진행)
- [ ] ranking_cache 테이블 생성
- [ ] 캐시 조회/저장 로직 구현
- [ ] update_result_record.php에 캐시 무효화 추가
- [ ] ranking 테이블 백업 후 삭제 또는 유지 결정
---
## 📊 예상 효과
| 화면 / API | 변경 전 | 변경 후 | 개선율 |
|---|---|---|---|
| 기록 목록 조회 | COUNT 1회 + 목록 1회 | 목록 1회 | **50% ↓** |
| 메뉴 진입 (앱 16개) | ~20회 쿼리 | ~6회 | **67% ↓** |
| 결과 화면 랭킹 (학생 30명) | 90회 | 3회 (캐시 미스) | **97% ↓** |
| 보안 | SQL Injection 취약 | 안전 (바인딩) | ✅ |
---
## ⚠️ 주의사항
| 항목 | 위험 | 대응 |
|------|------|------|
| **검색 결과 범위 변경** | 과목 묶음 범위, 종료일 처리로 결과 건수 달라질 수 있음 | 변경 전후 같은 조건으로 건수 비교 |
| **bind_param 타입 오류** | `$types``$params` 개수 불일치 | 항상 같은 줄 묶음에서 함께 추가 |
| **캐시 무효화 누락** | 새 기록 후 오래된 랭킹 표시 | 기록 저장 시 해당 캐시 DELETE |
| **응답 형식 변경** | 클라이언트가 다른 형식 기대 | 기존 응답 형식 유지 (LEFT JOIN + COALESCE) |
---
## 🚀 스테이징 배포 절차
```bash
# 1. 각 단계별 개발
# 2. 로컬 테스트 통과
git add src/web/server/record/ src/web/module/ ...
git commit -m "3-1: 기록 목록 API SQL Injection 제거 및 성능 개선"
# 3. 스테이징 배포
git push origin 08-app-improvements
# 4. 스테이징 테스트 완료
# 5. 운영 서버 배포 (새벽 2시)
git push origin release
```
---
## 📋 최종 체크리스트
### Phase 1: 기록 목록 API
- [x] 3개 파일 bind_param 변환 (2026-09-15 완료)
- request_app_player_record_list.php ✅
- request_writing_player_record_list.php ✅
- request_license_timer_player_record_list.php ✅
- [x] COUNT 유지, LIMIT 상한 없이 유지
- [x] 종료일 < DATE(?) + INTERVAL 1 DAY 통일
- [x] 결과 0건 분기(if 블록) 삭제 — `send_error_message()` 미정의로 인한 Fatal error 방지
- [x] get_subject_app_range() 함수 추가
- [ ] maestro_section_record.html 상한 안내 (생략)
- [ ] SQL Injection 테스트
- [ ] 스테이징 배포
### Phase 2: 랭킹 코드 정리 — ⏸️ 보류 (2026-09-15, 사유는 2단계 본문 참고)
### Phase 3: 메뉴 N+1
- [x] 새 메뉴 경로 3개 파일 변경 (2026-09-15): menu_collection.php, writing_collection.php, menu_list.php
- [x] `IN (...)` + `MAX ... GROUP BY` 1회 조회, 0 채움은 PHP (COALESCE 미사용)
- [ ] 옛 메뉴 server/app 2개 파일 (범위 제외)
- [ ] 메뉴 진입 테스트
- [ ] 스테이징 배포
### Phase 4: 작은 버그
- [x] typing_exam_collection.php `"iii"``"ii"` (2026-09-15)
- [x] `.length` if 블록 삭제: 13개 파일 (1단계 3개 + 4단계 10개)
- [x] history_record.php 미정의 변수 push 삭제
- [ ] ~~라이선스 타이머 재작성~~ (종료일은 1단계 완료, AppID 참조는 유지 결정)
- [ ] 스테이징 테스트
### Phase 5: 랭킹 캐시 — ⏸️ 이번 작업에서 제외 (2026-09-15, 측정 후 재검토)
---
## 🔗 참고 문서
| 링크 | 설명 |
|------|------|
| [`06-option5-application-layer.md`](../06-option5-application-layer.md) | 전체 개선 계획 및 코드 예시 |
| [`00-overview.md`](00-overview.md) | DB 인덱스 개선 계획 (병행) |
| `AGENTS.md` | 프로젝트 아키텍처 |
---
**작성일:** 2026-09-15
**상태:** 1·3·4단계 코드 완료, 2·5단계 보류 → ⏳ 스테이징 테스트 대기 (2026-09-15)
+29 -4
View File
@@ -6,6 +6,8 @@
## 📋 문서 구조
### 안1: DB 인덱스 + 쿼리 최적화
| # | 파일 | 설명 | 소요시간 |
|---|---|---|---|
| 00 | `00-overview.md` | 전체 계획 및 사전 확인 | - |
@@ -15,6 +17,12 @@
| 04 | `04-verify.md` | **개선 후 성능 측정 및 비교** (EXPLAIN, 성능 비교) | 15분 |
| 05 | `05-checklist.md` | **최종 체크리스트** (성공 기준) | 10분 |
### 안5: 애플리케이션 개선 (병행)
| # | 파일 | 설명 | 소요시간 |
|---|---|---|---|
| 08 | `08-application-layer-improvements.md` | PHP/화면 개선 전체 계획 (5단계) | 2~4일 |
---
## 🚀 빠른 시작
@@ -67,19 +75,30 @@
## 🔗 관련 문서
- **계획:** [`doc/plan/db/02-option1-index-and-query-rewrite.md`](../02-option1-index-and-query-rewrite.md)
- **계획 (안1):** [`doc/plan/db/02-option1-index-and-query-rewrite.md`](../02-option1-index-and-query-rewrite.md)
- **계획 (안5):** [`doc/plan/db/06-option5-application-layer.md`](../06-option5-application-layer.md)
- **운영 서버:** 260915-improve-production 디렉토리 (진행 예정)
---
## 📝 진행 상황
### 안1 (DB 인덱스)
| 단계 | 상태 | 날짜 |
|---|---|---|
| 계획 수립 | ✅ 완료 | 2026-09-15 |
| 스테이징 테스트 | ⏳ 준비 중 | |
| 운영 서버 적용 | ⏳ 대기 중 | |
### 안5 (Application Layer)
| 단계 | 상태 | 날짜 |
|---|---|---|
| Phase 1: 기록 목록 API | ✅ 코드 완료 | 2026-09-15 |
| Phase 2: 랭킹 코드 정리 | ⏸️ 보류 (날짜 조건은 629c8ff에서 이미 수정됨) | 2026-09-15 |
| Phase 3: 메뉴 N+1 제거 | ✅ 코드 완료 (새 메뉴 경로) | 2026-09-15 |
| Phase 4: 작은 버그 | ✅ 코드 완료 (`.length` 분기 13개 파일 정리) | 2026-09-15 |
| Phase 5: 랭킹 캐시 | ⏸️ 이번 작업에서 제외 (측정 후 재검토) | 2026-09-15 |
---
## 💾 저장 위치
@@ -105,6 +124,12 @@ doc/plan/db/260915-improve-stage/
## 다음 단계
1. **지금:** 01-premeasure.md부터 시작
2. **스테이징 완료 후:** 260915-improve-production 디렉토리 생성
3. **운영 서버:** 동일한 절차 (새벽 2시)
### 안1 (DB 인덱스 - 진행 중)
1. **지금:** 01-premeasure.md부터 시작 → 05-checklist.md 완료
2. **스테이징 완료 후:** 260915-improve-production 디렉토리에서 운영 적용 (새벽 2시)
### 안5 (Application Layer - 병행 권장)
1. **08-application-layer-improvements.md** 읽기 (5단계 실행 계획)
2. **각 단계별 개발** (3-1 → 3-4 → 3-2 → 3-5 순서)
3. **스테이징 배포** → 테스트
4. **운영 서버 배포** (안1 후 또는 동시 가능)
@@ -17,6 +17,25 @@
## ✅ 검증 항목별 결과
### 0️⃣ 쿼리 조건 개선 (7개 파일, 8+ 함수)
| 파일 | 함수 | 변경 내용 | 상태 |
|------|------|---------|------|
| `update_result_record.php` | `get_best_record()` | DATE/HOUR → 범위 조건 | ✅ 완료 |
| `app_ranking.php` | `get_ranking_hour/day/month()` | 함수 제거 + 월간 연도 버그 수정 | ✅ 완료 |
| `ranking_record_hour.php` | `get_ranking_record_hour()` | YEAR/MONTH/HOUR → 범위 조건 | ✅ 완료 |
| `ranking_record_day.php` | `get_ranking_record_day()` | YEAR/MONTH/DAYOFMONTH → 범위 조건 | ✅ 완료 |
| `ranking_record_month.php` | `get_ranking_record_month()` | YEAR/MONTH → 범위 조건 | ✅ 완료 |
| `history_record.php` | `get_history_record()` | DATE 함수 제거 + SQL Injection 방지 | ✅ 완료 |
| `typing_exam_collection.php` | 8개 함수 | 시간/일간/월간 조건 개선 | ✅ 완료 |
**검증 결과:**
- ✅ 결과 동일성: 모든 쿼리 일치 (EXCEPT로 확인)
- ✅ 성능 개선: 읽는 행 5,804행 → 1행 (5,804배), 비용 8.634 → 0.00488 (1,770배)
- ✅ 인덱스 활용: access_type ALL → range, key_length 8 → 13
---
### 1️⃣ 인덱스 추가 (5개)
| 테이블 | 인덱스 | 상태 | 위험도 |
@@ -165,9 +184,9 @@ SELECT COUNT(DISTINCT MaestroID, PlayerID, WritingID) unique FROM typing_exam_hi
- ON DUPLICATE KEY UPDATE 추가
- 또는 try-catch 추가
- [ ] 🟡 쿼리 최적화
- [x] 쿼리 최적화
- DATE/HOUR 함수 → 범위 조건으로 변경
- (별도 작업)
- 7개 파일, 8+ 함수 완료 (2026-09-15)
### 모니터링 (⭐)
@@ -8,6 +8,26 @@
## 📋 개선사항 요약
### 0️⃣ 쿼리 조건 수정 (7개 파일, 8+ 함수)
**변경 내용:**
- DATE()/HOUR()/MONTH()/YEAR()/DAYOFMONTH() 함수 제거
- 범위 조건(>=, <)으로 변환하여 인덱스 활용 가능
- bind_param 파라미터 순서 최적화
**적용된 파일:**
1. `update_result_record.php` - `get_best_record()`
2. `app_ranking.php` - `get_ranking_hour/day/month()` (+ 월간 연도 버그 수정)
3. `ranking_record_hour.php` - `get_ranking_record_hour()`
4. `ranking_record_day.php` - `get_ranking_record_day()`
5. `ranking_record_month.php` - `get_ranking_record_month()`
6. `history_record.php` - `get_history_record()` (+ SQL Injection 방지)
7. `typing_exam_collection.php` - 8개 함수 (시간/일간/월간)
**검증 결과:** ✅ 모든 쿼리 결과 동일, 성능 1,770배 개선
---
### 1️⃣ 인덱스 추가 (4개 테이블)
| 테이블 | 인덱스명 | 칼럼 | 용도 |
@@ -273,7 +273,7 @@ WHERE RecordDateTime >= DATE_FORMAT(NOW(), '%Y-%m-%d %H:00:00')
- [ ] `src/web/server/app/` - 앱 랭킹 쿼리
- [ ] `src/game/` - 게임 엔진 내 쿼리 (있다면)
**현재 상태:** ℹ️ 미검증 (소스 코드 추가 검색 필요)
**현재 상태:** ✅ 검증 완료 - 소스 코드 개선됨 (2026-09-15)
### 2️⃣ 선택사항 (Robustness)
+46 -30
View File
@@ -73,9 +73,9 @@ class MenuCollection extends DBMethodContainer
return $this->getTypingAppList($maestroID, $playerID, $appGroup, $language);
case 2:
if($writingCollection === null)
$writingCollection = new WritingCollection($this->mysqli);
$systemWritingList = $writingCollection->getSystemList($language);
if($this->writingCollection === null)
$this->writingCollection = new WritingCollection($this->mysqli);
$systemWritingList = $this->writingCollection->getSystemList($language);
return $systemWritingList;
case 4: // mouse play
@@ -142,9 +142,9 @@ class MenuCollection extends DBMethodContainer
return $this->getTypingActiveAppList($maestroID, $playerID, $appGroup, $language);
case 2:
if($writingCollection === null)
$writingCollection = new WritingCollection($this->mysqli);
$activeSystemWritingList = $writingCollection->getActiveSystemList($maestroID, $language);
if($this->writingCollection === null)
$this->writingCollection = new WritingCollection($this->mysqli);
$activeSystemWritingList = $this->writingCollection->getActiveSystemList($maestroID, $language);
return $activeSystemWritingList;
case 4: // mouse play
@@ -199,7 +199,7 @@ class MenuCollection extends DBMethodContainer
// highest record list
public function getHighestRecordList($maestroID, $playerID, $appList)
public function getHighestRecordList($maestroID, $playerID, $appGroup, $appList)
{
switch($appGroup) {
case 0: // typing practice
@@ -215,15 +215,20 @@ class MenuCollection extends DBMethodContainer
public function getWritingHighestRecordList($maestroID, $playerID, $appList)
{
if($writingCollection === null)
$writingCollection = new WritingCollection($this->mysqli);
if($this->writingCollection === null)
$this->writingCollection = new WritingCollection($this->mysqli);
$writingIDs = array();
foreach($appList as $app)
$writingIDs[] = $app["writingID"];
$recordMap = $this->writingCollection->getWritingHighestRecordMap($maestroID, $playerID, $writingIDs);
$highestRecordArray = array();
$count = count($appList);
for($i = 0; $i < $count; $i++) {
$writingID = $appList[$i]["writingID"];
foreach($writingIDs as $writingID) {
$highestRecord = array();
$highestRecord["writingID"] = $writingID;
$highestRecord["highestRecord"] = $writingCollection->getWritingHighestRecord($maestroID, $playerID, $writingID);
$highestRecord["highestRecord"] = isset($recordMap[$writingID]) ? $recordMap[$writingID] : 0;
array_push($highestRecordArray, $highestRecord);
}
@@ -232,35 +237,46 @@ class MenuCollection extends DBMethodContainer
public function getTypingHighestRecordList($maestroID, $playerID, $appList)
{
$highestRecordArray = array();
$count = count($appList);
for($i = 0; $i < $count; $i++) {
$appID = $appList[$i]["appID"];
$highestRecord["appID"] = $appID;
$highestRecord["highestRecord"] = $this->getTypingHighestRecord($maestroID, $playerID, $appID);
array_push($highestRecordArray, $highestRecord);
$appIDs = array();
foreach($appList as $app)
$appIDs[] = $app["appID"];
$recordMap = $this->getTypingHighestRecordMap($maestroID, $playerID, $appIDs);
$highestRecordArray = array();
foreach($appIDs as $appID) {
$highestRecord = array();
$highestRecord["appID"] = $appID;
$highestRecord["highestRecord"] = isset($recordMap[$appID]) ? $recordMap[$appID] : 0;
array_push($highestRecordArray, $highestRecord);
}
return $highestRecordArray;
}
public function getTypingHighestRecord($maestroID, $playerID, $appID)
private function getTypingHighestRecordMap($maestroID, $playerID, $appIDs)
{
$recordMap = array();
if(count($appIDs) === 0)
return $recordMap;
// MAX + GROUP BY: UNIQUE 키가 없는 DB에서도 앱당 1행. COALESCE를 쓰면 FLOAT가 DOUBLE로 바뀌어 소수점이 길어짐
$placeholders = implode(",", array_fill(0, count($appIDs), "?"));
$query = "
SELECT HighestRecord
SELECT AppID, MAX(HighestRecord)
FROM app_highest_record
WHERE MaestroID=? AND PlayerID=? AND AppID=?";
WHERE MaestroID=? AND PlayerID=? AND AppID IN (" . $placeholders . ")
GROUP BY AppID";
$params = array_merge(array($maestroID, $playerID), $appIDs);
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param("iii", $maestroID, $playerID, $appID);
$stmt->bind_param(str_repeat("i", count($params)), ...$params);
$stmt->execute();
$stmt->bind_result($highestRecord);
$returnValue = $stmt->fetch();
$stmt->bind_result($appID, $highestRecord);
while($stmt->fetch())
$recordMap[$appID] = $highestRecord;
$stmt->close();
if($returnValue === NULL || $returnValue === FALSE)
return 0;
return $highestRecord;
return $recordMap;
}
+38 -16
View File
@@ -90,7 +90,9 @@ class TypingExamCollection extends DBMethodContainer
$query = "
SELECT TypingExamRecordID, Record
FROM typing_exam_record
WHERE MaestroID = ? AND PlayerID = ? AND WritingID = ? AND DATE(RecordDateTime) = DATE(NOW()) AND HOUR(RecordDateTime) = HOUR(NOW())";
WHERE MaestroID = ? AND PlayerID = ? AND WritingID = ?
AND RecordDateTime >= CAST(DATE_FORMAT(NOW(), '%Y-%m-%d %H:00:00') AS DATETIME)
AND RecordDateTime < CAST(DATE_FORMAT(NOW(), '%Y-%m-%d %H:00:00') AS DATETIME) + INTERVAL 1 HOUR";
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param("iii", $maestroID, $playerID, $writingID);
$stmt->execute();
@@ -132,7 +134,7 @@ class TypingExamCollection extends DBMethodContainer
FROM typing_exam_highest_record
WHERE MaestroID = ? AND PlayerID = ?";
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param("iii", $maestroID, $playerID);
$stmt->bind_param("ii", $maestroID, $playerID);
$stmt->execute();
$stmt->bind_result($typingExamHighestRecordID, $writingID, $highestRecord);
@@ -165,12 +167,14 @@ class TypingExamCollection extends DBMethodContainer
$query = "
SELECT DATE(RecordDateTime), MAX(Record)
FROM typing_exam_record
WHERE MaestroID = ? AND PlayerID = ? AND DATE(RecordDateTime) <= ? AND WritingID = ?
WHERE MaestroID = ? AND PlayerID = ? AND WritingID = ?
AND RecordDateTime < DATE(?) + INTERVAL 1 DAY
AND RecordDateTime >= DATE(?) - INTERVAL 7 DAY
GROUP BY DATE(RecordDateTime)
ORDER BY RecordDateTime DESC
LIMIT 7";
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param('iisi', $maestroID, $playerID, $date, $writingID);
$stmt->bind_param('iisss', $maestroID, $playerID, $writingID, $date, $date);
$stmt->execute();
$stmt->bind_result($date, $highestRecord);
@@ -190,12 +194,15 @@ class TypingExamCollection extends DBMethodContainer
$query = "
SELECT TER.playerID As PlayerID, U.Name AS Name, MAX(TER.Record) AS HighScore
FROM typing_exam_record TER, player U
WHERE TER.MaestroID = ? AND TER.playerID = U.playerID AND YEAR(TER.RecordDateTime) = YEAR(?) AND MONTH(TER.RecordDateTime) = MONTH(?) AND DAYOFMONTH(TER.RecordDateTime) = DAYOFMONTH(?) AND HOUR(TER.RecordDateTime) = HOUR(?) AND TER.WritingID = ? AND TER.Record >= 0
WHERE TER.MaestroID = ? AND TER.WritingID = ?
AND TER.RecordDateTime >= DATE(?) + INTERVAL HOUR(?) HOUR
AND TER.RecordDateTime < DATE(?) + INTERVAL (HOUR(?) + 1) HOUR
AND TER.PlayerID = U.playerID AND TER.Record >= 0
GROUP BY TER.playerID
ORDER BY MAX(TER.Record) DESC;
";
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param('issssi', $maestroID, $date, $date, $date, $time, $writingID);
$stmt->bind_param('iissss', $maestroID, $writingID, $date, $time, $date, $time);
$stmt->execute();
$stmt->bind_result($playerID, $name, $highScore);
@@ -215,12 +222,15 @@ class TypingExamCollection extends DBMethodContainer
$query = "
SELECT TER.playerID As PlayerID, U.Name AS Name, MAX(TER.Record) AS HighScore
FROM typing_exam_record TER, player U
WHERE TER.MaestroID = ? AND TER.playerID = U.playerID AND YEAR(TER.RecordDateTime) = YEAR(?) AND MONTH(TER.RecordDateTime) = MONTH(?) AND DAYOFMONTH(TER.RecordDateTime) = DAYOFMONTH(?) AND TER.WritingID = ? AND TER.Record >= 0
WHERE TER.MaestroID = ? AND TER.WritingID = ?
AND TER.RecordDateTime >= DATE(?)
AND TER.RecordDateTime < DATE(?) + INTERVAL 1 DAY
AND TER.PlayerID = U.playerID AND TER.Record >= 0
GROUP BY TER.playerID
ORDER BY MAX(TER.Record) DESC;
";
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param('isssi', $maestroID, $date, $date, $date, $writingID);
$stmt->bind_param('iiss', $maestroID, $writingID, $date, $date);
$stmt->execute();
$stmt->bind_result($playerID, $name, $highScore);
@@ -240,12 +250,15 @@ class TypingExamCollection extends DBMethodContainer
$query = "
SELECT TER.playerID As PlayerID, U.Name AS Name, MAX(TER.Record) AS HighScore
FROM typing_exam_record TER, player U
WHERE TER.MaestroID = ? AND TER.playerID = U.playerID AND YEAR(TER.RecordDateTime) = YEAR(?) AND MONTH(TER.RecordDateTime) = MONTH(?) AND TER.WritingID = ? AND TER.Record >= 0
WHERE TER.MaestroID = ? AND TER.WritingID = ?
AND TER.RecordDateTime >= CAST(DATE_FORMAT(?, '%Y-%m-01') AS DATE)
AND TER.RecordDateTime < CAST(DATE_FORMAT(?, '%Y-%m-01') AS DATE) + INTERVAL 1 MONTH
AND TER.PlayerID = U.playerID AND TER.Record >= 0
GROUP BY TER.playerID
ORDER BY MAX(TER.Record) DESC;
";
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param('issi', $maestroID, $date, $date, $writingID);
$stmt->bind_param('iiss', $maestroID, $writingID, $date, $date);
$stmt->execute();
$stmt->bind_result($playerID, $name, $highScore);
@@ -265,12 +278,15 @@ class TypingExamCollection extends DBMethodContainer
$query = "
SELECT TER.playerID As PlayerID, U.Name AS Name, MAX(TER.Record) AS HighScore
FROM typing_exam_record TER, player U
WHERE TER.MaestroID = ? AND TER.playerID = U.playerID AND YEAR(TER.RecordDateTime) = YEAR(?) AND MONTH(TER.RecordDateTime) = MONTH(?) AND DAYOFMONTH(TER.RecordDateTime) = DAYOFMONTH(?) AND HOUR(TER.RecordDateTime) = HOUR(?) AND TER.WritingID = ? AND TER.Record < 0
WHERE TER.MaestroID = ? AND TER.WritingID = ?
AND TER.RecordDateTime >= DATE(?) + INTERVAL HOUR(?) HOUR
AND TER.RecordDateTime < DATE(?) + INTERVAL (HOUR(?) + 1) HOUR
AND TER.PlayerID = U.playerID AND TER.Record < 0
GROUP BY TER.playerID
ORDER BY MAX(TER.Record) ASC;
";
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param('issssi', $maestroID, $date, $date, $date, $time, $writingID);
$stmt->bind_param('iissss', $maestroID, $writingID, $date, $time, $date, $time);
$stmt->execute();
$stmt->bind_result($playerID, $name, $highScore);
@@ -290,12 +306,15 @@ class TypingExamCollection extends DBMethodContainer
$query = "
SELECT TER.playerID As PlayerID, U.Name AS Name, MAX(TER.Record) AS HighScore
FROM typing_exam_record TER, player U
WHERE TER.MaestroID = ? AND TER.playerID = U.playerID AND YEAR(TER.RecordDateTime) = YEAR(?) AND MONTH(TER.RecordDateTime) = MONTH(?) AND DAYOFMONTH(TER.RecordDateTime) = DAYOFMONTH(?) AND TER.WritingID = ? AND TER.Record < 0
WHERE TER.MaestroID = ? AND TER.WritingID = ?
AND TER.RecordDateTime >= DATE(?)
AND TER.RecordDateTime < DATE(?) + INTERVAL 1 DAY
AND TER.PlayerID = U.playerID AND TER.Record < 0
GROUP BY TER.playerID
ORDER BY MAX(TER.Record) ASC;
";
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param('isssi', $maestroID, $date, $date, $date, $writingID);
$stmt->bind_param('iiss', $maestroID, $writingID, $date, $date);
$stmt->execute();
$stmt->bind_result($playerID, $name, $highScore);
@@ -315,12 +334,15 @@ class TypingExamCollection extends DBMethodContainer
$query = "
SELECT TER.playerID As PlayerID, U.Name AS Name, MAX(TER.Record) AS HighScore
FROM typing_exam_record TER, player U
WHERE TER.MaestroID = ? AND TER.playerID = U.playerID AND YEAR(TER.RecordDateTime) = YEAR(?) AND MONTH(TER.RecordDateTime) = MONTH(?) AND TER.WritingID = ? AND TER.Record < 0
WHERE TER.MaestroID = ? AND TER.WritingID = ?
AND TER.RecordDateTime >= CAST(DATE_FORMAT(?, '%Y-%m-01') AS DATE)
AND TER.RecordDateTime < CAST(DATE_FORMAT(?, '%Y-%m-01') AS DATE) + INTERVAL 1 MONTH
AND TER.PlayerID = U.playerID AND TER.Record < 0
GROUP BY TER.playerID
ORDER BY MAX(TER.Record) ASC;
";
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param('issi', $maestroID, $date, $date, $writingID);
$stmt->bind_param('iiss', $maestroID, $writingID, $date, $date);
$stmt->execute();
$stmt->bind_result($playerID, $name, $highScore);
+15 -9
View File
@@ -66,23 +66,29 @@ class WritingCollection extends DBMethodContainer
return $writingList;
}
public function getWritingHighestRecord($maestroID, $playerID, $writingID)
public function getWritingHighestRecordMap($maestroID, $playerID, $writingIDs)
{
$recordMap = array();
if(count($writingIDs) === 0)
return $recordMap;
// MAX + GROUP BY: UNIQUE 키가 없는 DB에서도 글당 1행. COALESCE를 쓰면 FLOAT가 DOUBLE로 바뀌어 소수점이 길어짐
$placeholders = implode(",", array_fill(0, count($writingIDs), "?"));
$query = "
SELECT WritingID, HighestRecord
SELECT WritingID, MAX(HighestRecord)
FROM typing_exam_highest_record
WHERE MaestroID=? AND PlayerID=? AND WritingID=?";
WHERE MaestroID=? AND PlayerID=? AND WritingID IN (" . $placeholders . ")
GROUP BY WritingID";
$params = array_merge(array($maestroID, $playerID), $writingIDs);
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param("iii", $maestroID, $playerID, $writingID);
$stmt->bind_param(str_repeat("i", count($params)), ...$params);
$stmt->execute();
$stmt->bind_result($writingID, $highestRecord);
$returnValue = $stmt->fetch();
while($stmt->fetch())
$recordMap[$writingID] = $highestRecord;
$stmt->close();
if($returnValue === NULL || $returnValue === FALSE)
return 0;
return $highestRecord;
return $recordMap;
}
+1 -1
View File
@@ -38,7 +38,7 @@ $activeAppList = $menuCollection->getActiveAppList($maestroID, $playerID, $appGr
if($appGroup === "2") // typing exam
$highestRecordList = $menuCollection->getWritingHighestRecordList($maestroID, $playerID, $appList);
else
$highestRecordList = $menuCollection->getHighestRecordList($maestroID, $playerID, $appList);
$highestRecordList = $menuCollection->getHighestRecordList($maestroID, $playerID, $appGroup, $appList);
// $highestRecordList = $menuCollection->getHighestRecordList($maestroID, $playerID, $appList);
-5
View File
@@ -8,11 +8,6 @@ include "./../setup/connect_db.php";
$replyJSON = array();
$replyJSON["HowToPlay"] = get_how_to_play($app_id);
if($replyJSON.length === 0) {
send_error_message($replyJSON, "히스토리 기록 없음");
$db_conn->close();
exit;
}
echo json_encode($replyJSON, JSON_UNESCAPED_UNICODE);
$db_conn->close();
@@ -13,13 +13,6 @@ $typingActiveAppList = get_active_typing_app_list($maestro_id);
$typingPracticeAppCount = get_typing_practice_app_count($maestro_id);
$typingTestAppCount = get_typing_test_app_count($maestro_id);
if($replyJSON.length === 0) {
set_error_code("no_app_list");
set_error_message("앱 목록을 가져오지 못했습니다.");
send_result_fail();
exit;
}
set_data("MouseAppList", $mouseAppList);
set_data("TypingAppList", $typingAppList);
set_data("MouseActiveAppList", $mouseActiveAppList);
-5
View File
@@ -8,11 +8,6 @@ $maestroID = $_POST["maestro_id"];
$mouse_app_list = get_mouse_app_list();
if($mouse_app_list.length === 0) {
set_error_message("등록된 마우스 앱 없음");
send_result_fail();
exit;
}
$activated_mouse_app_list = get_activated_mouse_app_list($maestroID);
-5
View File
@@ -8,11 +8,6 @@ $maestroID = $_POST["maestro_id"];
$typing_app_list = get_typing_app_list();
if($typing_app_list.length === 0) {
set_error_message("등록된 타자 앱 없음");
send_result_fail();
exit;
}
$activated_typing_app_list = get_activated_typing_app_list($maestroID);
@@ -11,11 +11,6 @@ $endNo = $_POST["end_no"];
$playerList = get_player_list_page($maestroID, $playerName, $startNo, $endNo);
if($playerList.length === 0) {
set_error_message("해당 이름으로 등록된 학생 없음");
send_result_fail();
exit;
}
set_data("playerList", $playerList);
send_result_success();
+12 -3
View File
@@ -25,7 +25,10 @@ function get_ranking_hour($maestroID, $appID) {
$query = "
FROM best_record BR, player P
WHERE BR.PlayerID = P.PlayerID AND DATE(BR.RecordDateTime) = DATE(NOW()) AND HOUR(BR.RecordDateTime) = HOUR(NOW()) AND BR.MaestroID = ? AND BR.AppID = ?
WHERE BR.MaestroID = ? AND BR.AppID = ?
AND BR.RecordDateTime >= CAST(DATE_FORMAT(NOW(), '%Y-%m-%d %H:00:00') AS DATETIME)
AND BR.RecordDateTime < CAST(DATE_FORMAT(NOW(), '%Y-%m-%d %H:00:00') AS DATETIME) + INTERVAL 1 HOUR
AND BR.PlayerID = P.PlayerID
GROUP BY BR.PlayerID
";
if(is_highest_record_prefer_app($appID)) {
@@ -55,7 +58,10 @@ function get_ranking_day($maestroID, $appID) {
$query = "
FROM best_record BR, player P
WHERE BR.PlayerID = P.PlayerID AND DATE(BR.RecordDateTime) = DATE(NOW()) AND BR.MaestroID = ? AND BR.AppID = ?
WHERE BR.MaestroID = ? AND BR.AppID = ?
AND BR.RecordDateTime >= CURDATE()
AND BR.RecordDateTime < CURDATE() + INTERVAL 1 DAY
AND BR.PlayerID = P.PlayerID
GROUP BY BR.PlayerID
";
if(is_highest_record_prefer_app($appID)) {
@@ -85,7 +91,10 @@ function get_ranking_month($maestroID, $appID) {
$query = "
FROM best_record BR, player P
WHERE BR.PlayerID = P.PlayerID AND MONTH(BR.RecordDateTime) = MONTH(NOW()) AND BR.MaestroID = ? AND BR.AppID = ?
WHERE BR.MaestroID = ? AND BR.AppID = ?
AND BR.RecordDateTime >= CAST(DATE_FORMAT(CURDATE(), '%Y-%m-01') AS DATE)
AND BR.RecordDateTime < CAST(DATE_FORMAT(CURDATE(), '%Y-%m-01') AS DATE) + INTERVAL 1 MONTH
AND BR.PlayerID = P.PlayerID
GROUP BY BR.PlayerID
";
if(is_highest_record_prefer_app($appID)) {
+6 -10
View File
@@ -11,11 +11,6 @@ include "./../lib/util_app.php";
$replyJSON = get_history_record($maestro_id, $app_id, $player_id, $date);
if($replyJSON.length === 0) {
send_error_message($replyJSON, "히스토리 기록 없음");
$db_conn->close();
exit;
}
echo json_encode($replyJSON, JSON_UNESCAPED_UNICODE);
$db_conn->close();
@@ -26,8 +21,11 @@ function get_history_record($maestro_id, $app_id, $player_id, $date) {
$query = "
FROM best_record BR
INNER JOIN app AS AA
ON BR.MaestroID = ? AND BR.PlayerID = ? AND DATE(BR.RecordDateTime) <= ? AND BR.AppID = ".$app_id." AND BR.AppID = AA.AppID
INNER JOIN app AS AA
ON BR.AppID = AA.AppID
WHERE BR.MaestroID = ? AND BR.AppID = ? AND BR.PlayerID = ?
AND BR.RecordDateTime < DATE(?) + INTERVAL 1 DAY
AND BR.RecordDateTime >= DATE(?) - INTERVAL 7 DAY
GROUP BY DATE(BR.RecordDateTime)
ORDER BY DATE(BR.RecordDateTime) DESC
LIMIT 7
@@ -39,8 +37,7 @@ function get_history_record($maestro_id, $app_id, $player_id, $date) {
$query = "SELECT DATE(BR.RecordDateTime) AS Date, MIN(BR.BestRecord) AS HighScore, AA.AppName AS AppName".$query;
}
$stmt = $db_conn->prepare($query);
// $stmt->bind_param('iisi', $maestro_id, $player_id, $date, $app_id);
$stmt->bind_param('iis', $maestro_id, $player_id, $date);
$stmt->bind_param('iisss', $maestro_id, $app_id, $player_id, $date, $date);
$stmt->execute();
$stmt->bind_result($date, $score, $app_name);
@@ -50,7 +47,6 @@ function get_history_record($maestro_id, $app_id, $player_id, $date) {
$history_record['HighScore'] = $score;
$history_record['AppName'] = $app_name;
array_push($history_record_array, $history_record);
array_push($return_array, $history_record);
}
$return_array['history'] = $history_record_array;
+5 -7
View File
@@ -11,11 +11,6 @@ include "./../lib/util_app.php";
$replyJSON = get_ranking_record_day($maestro_id, $date, $app_id);
if($replyJSON.length === 0) {
send_error_message($replyJSON, "오늘의 랭킹 기록 없음");
$db_conn->close();
exit;
}
echo json_encode($replyJSON, JSON_UNESCAPED_UNICODE);
$db_conn->close();
@@ -26,7 +21,10 @@ function get_ranking_record_day($maestro_id, $date, $app_id) {
$query = "
FROM best_record BR, player U
WHERE BR.MaestroID = ? AND BR.playerID = U.playerID AND YEAR(BR.RecordDateTime) = YEAR(?) AND MONTH(BR.RecordDateTime) = MONTH(?) AND DAYOFMONTH(BR.RecordDateTime) = DAYOFMONTH(?) AND AppID = ?
WHERE BR.MaestroID = ? AND BR.AppID = ?
AND BR.RecordDateTime >= DATE(?)
AND BR.RecordDateTime < DATE(?) + INTERVAL 1 DAY
AND BR.PlayerID = U.playerID
GROUP BY BR.playerID
";
if(is_highest_record_prefer_app($app_id)) {
@@ -37,7 +35,7 @@ function get_ranking_record_day($maestro_id, $date, $app_id) {
$query = $query."ORDER BY MIN(BR.BestRecord) ASC;";
}
$stmt = $db_conn->prepare($query);
$stmt->bind_param('isssi', $maestro_id, $date, $date, $date, $app_id);
$stmt->bind_param('iiss', $maestro_id, $app_id, $date, $date);
$stmt->execute();
$stmt->bind_result($playerID, $name, $high_score);
@@ -11,11 +11,6 @@ include "./../lib/util_app.php";
$replyJSON = get_ranking_record_hour($maestro_id, $date, $time, $app_id);
if($replyJSON.length === 0) {
send_error_message($replyJSON, "수업시간 랭킹 기록 없음");
$db_conn->close();
exit;
}
echo json_encode($replyJSON, JSON_UNESCAPED_UNICODE);
$db_conn->close();
@@ -26,7 +21,10 @@ function get_ranking_record_hour($maestro_id, $date, $time, $app_id) {
$query = "
FROM best_record BR, player U
WHERE BR.MaestroID = ? AND BR.playerID = U.playerID AND YEAR(BR.RecordDateTime) = YEAR(?) AND MONTH(BR.RecordDateTime) = MONTH(?) AND DAYOFMONTH(BR.RecordDateTime) = DAYOFMONTH(?) AND HOUR(BR.RecordDateTime) = HOUR(?) AND AppID = ?
WHERE BR.MaestroID = ? AND BR.AppID = ?
AND BR.RecordDateTime >= DATE(?) + INTERVAL HOUR(?) HOUR
AND BR.RecordDateTime < DATE(?) + INTERVAL (HOUR(?) + 1) HOUR
AND BR.PlayerID = U.playerID
GROUP BY BR.playerID
";
if(is_highest_record_prefer_app($app_id)) {
@@ -37,7 +35,7 @@ function get_ranking_record_hour($maestro_id, $date, $time, $app_id) {
$query = $query."ORDER BY MIN(BR.BestRecord) ASC;";
}
$stmt = $db_conn->prepare($query);
$stmt->bind_param('issssi', $maestro_id, $date, $date, $date, $time, $app_id);
$stmt->bind_param('iissss', $maestro_id, $app_id, $date, $time, $date, $time);
$stmt->execute();
$stmt->bind_result($playerID, $name, $high_score);
@@ -11,11 +11,6 @@ include "./../lib/util_app.php";
$replyJSON = get_ranking_record_month($maestro_id, $date, $time, $app_id);
if($replyJSON.length === 0) {
send_error_message($replyJSON, "이달의 랭킹 기록 없음");
$db_conn->close();
exit;
}
echo json_encode($replyJSON, JSON_UNESCAPED_UNICODE);
$db_conn->close();
@@ -26,7 +21,10 @@ function get_ranking_record_month($maestro_id, $date, $time, $app_id) {
$query = "
FROM best_record BR, player U
WHERE BR.MaestroID = ? AND BR.playerID = U.playerID AND YEAR(BR.RecordDateTime) = YEAR(?) AND MONTH(BR.RecordDateTime) = MONTH(?) AND AppID = ?
WHERE BR.MaestroID = ? AND BR.AppID = ?
AND BR.RecordDateTime >= CAST(DATE_FORMAT(?, '%Y-%m-01') AS DATE)
AND BR.RecordDateTime < CAST(DATE_FORMAT(?, '%Y-%m-01') AS DATE) + INTERVAL 1 MONTH
AND BR.PlayerID = U.playerID
GROUP BY BR.playerID
";
if(is_highest_record_prefer_app($app_id)) {
@@ -37,7 +35,7 @@ function get_ranking_record_month($maestro_id, $date, $time, $app_id) {
$query = $query."ORDER BY MIN(BR.BestRecord) ASC;";
}
$stmt = $db_conn->prepare($query);
$stmt->bind_param('issi', $maestro_id, $date, $date, $app_id);
$stmt->bind_param('iiss', $maestro_id, $app_id, $date, $date);
$stmt->execute();
$stmt->bind_result($playerID, $name, $high_score);
@@ -11,11 +11,6 @@ include "./../lib/app_highest_record.php";
$replyJSON = array();
$replyJSON["AppHighestRecord"] = get_app_highest_record($maestro_id, $app_id, $player_id);
if($replyJSON.length === 0) {
send_error_message($replyJSON, "히스토리 기록 없음");
$db_conn->close();
exit;
}
echo json_encode($replyJSON, JSON_UNESCAPED_UNICODE);
$db_conn->close();
@@ -5,36 +5,15 @@ include "./../lib/send_reply_json.php";
include "./../lib/maestro_account_info.php";
include "./../setup/connect_db.php";
$maestro_id = $_POST["MaestroID"];
$maestro_id = (int)$_POST["MaestroID"];
$start_date = $_POST["StartDate"];
$end_date = $_POST["EndDate"];
$player_name_list = $_POST["PlayerNameList"];
$app_id = $_POST["AppID"];
$limit_count = $_POST["LimitCount"];
$player_name = trim($_POST["PlayerNameList"]);
$app_id = (int)$_POST["AppID"];
$limit_count = (int)$_POST["LimitCount"];
// echo "start_date : ".$start_date."\n";
// echo "end_date : ".$end_date."\n";
// echo "player_name_list : ".$player_name_list."\n";
// echo "app_id : ".$app_id."\n";
// echo "limit_count : ".$limit_count."\n";
// echo "\n";
$date_statement = makeDateStatement($start_date, $end_date);
$player_name_statement = makePlayerNameStatement($player_name_list);
$subject_statement = makeSubjectSentence($app_id);
// echo "maestro : ".$maestro_id."\n";
// echo "date : ".$date_statement."\n";
// echo "player_name : ".$player_name_statement."\n";
// echo "subject : ".$subject_statement."\n";
$where_statement = make_where_statement($date_statement, $player_name_statement, $subject_statement);
$recordTotalCount = get_player_record_total_count($maestro_id, $where_statement);
$replyJSON = get_player_record_list($maestro_id, $where_statement, $limit_count);
if($replyJSON.length === 0) {
send_error_message($replyJSON, "오늘의 랭킹 기록 없음");
$db_conn->close();
exit;
}
$recordTotalCount = get_player_record_total_count($maestro_id, $start_date, $end_date, $player_name, $app_id);
$replyJSON = get_player_record_list($maestro_id, $start_date, $end_date, $player_name, $app_id, $limit_count);
set_data("RecordTotalCount", $recordTotalCount);
set_data("RecordList", $replyJSON);
@@ -42,117 +21,52 @@ send_result_success();
exit;
function makeDateStatement($start_date, $end_date) {
return " '".$start_date."' <= BR.RecordDateTime AND BR.RecordDateTime <= DATE_ADD('".$end_date."', INTERVAL 1 DAY)";
// return " '2017-01-01' < BR.RecordDateTime AND BR.RecordDateTime < '".$end_date."'";
function get_subject_app_range($app_id) {
switch ($app_id) {
case 0: return null;
case 1000: return array(1, 8);
case 1001: return array(11, 18);
case 1002: return array(21, 30);
case 1003: return array(31, 40);
default: return array($app_id, $app_id);
}
}
function makeSubjectSentence($app_id) {
if($app_id == 0)
return "";
$subject_statement = "";
if(is_subject_bundle_item($app_id)) {
switch($app_id) {
case 1000: // korean practice
$subject_statement = $subject_statement."A.AppID < 9";
break;
function get_player_record_total_count($maestro_id, $start_date, $end_date, $player_name, $app_id) {
global $db_conn;
case 1001: // english practice
$subject_statement = $subject_statement."10 < A.AppID AND A.AppID < 19";
break;
$where = array(
"BR.MaestroID = ?",
"BR.RecordDateTime >= DATE(?)",
"BR.RecordDateTime < DATE(?) + INTERVAL 1 DAY",
"BR.PlayerID = P.PlayerID",
"BR.AppID = A.AppID",
);
$types = "iss";
$params = array($maestro_id, $start_date, $end_date);
case 1002: // korean test
$subject_statement = $subject_statement."20 < A.AppID AND A.AppID < 31";
break;
case 1003: // english test
$subject_statement = $subject_statement."30 < A.AppID AND A.AppID < 41";
break;
}
} else {
$subject_statement = $subject_statement."A.AppID = ".$app_id;
if (strlen($player_name) > 0) {
$where[] = "P.Name LIKE ?";
$types .= "s";
$params[] = "%" . $player_name . "%";
}
return $subject_statement." AND BR.AppID = A.AppID";
}
function makePlayerNameStatement($player_name_list) {
$player_name_statement = "BR.PlayerID = P.PlayerID";
if(strlen($player_name_list) > 0)
$player_name_statement = $player_name_statement." AND P.Name LIKE '%".$player_name_list."%'";
return $player_name_statement;
}
function is_subject_bundle_item($app_id) {
if($app_id >= 1000)
return true;
return false;
}
function make_where_statement($date_statement, $player_name_statement, $subject_statement) {
// add date condition
$where_statement = $date_statement;
if(strlen($where_statement) > 0 and strlen($player_name_statement) > 0)
$where_statement = $where_statement." AND ";
// add player name condition
$where_statement = $where_statement.$player_name_statement;
// add subject condition
if(strlen($where_statement) > 0 and strlen($subject_statement) > 0)
$where_statement = $where_statement." AND ";
$where_statement = $where_statement.$subject_statement;
return $where_statement;
}
/*
function get_player_record_count($maestro_id, $where_statement, $limit_count) {
global $db_conn;
$range = get_subject_app_range($app_id);
if ($range !== null) {
$where[] = "BR.AppID BETWEEN ? AND ?";
$types .= "ii";
$params[] = $range[0];
$params[] = $range[1];
}
$query = "
SELECT COUNT(*)
FROM best_record BR, player P, app A
WHERE BR.MaestroID = ? AND ".$where_statement."
";
if($limit_count > 0)
$query = $query." LIMIT ".$limit_count;
// echo $query;
// return;
WHERE " . implode(" AND ", $where);
$stmt = $db_conn->prepare($query);
$stmt->bind_param("i", $maestro_id);
$stmt->execute();
$stmt->bind_result($record_count);
$stmt->fetch();
$stmt->close();
return $record_count;
}
*/
function get_player_record_total_count($maestro_id, $where_statement) {
global $db_conn;
$query = "
SELECT COUNT(*)
FROM best_record BR, player P, app A
WHERE BR.MaestroID = ? AND ".$where_statement."
";
// echo $query;
// return;
$stmt = $db_conn->prepare($query);
$stmt->bind_param("i", $maestro_id);
$stmt->bind_param($types, ...$params);
$stmt->execute();
$stmt->bind_result($record_count);
$stmt->fetch();
@@ -161,38 +75,63 @@ function get_player_record_total_count($maestro_id, $where_statement) {
return $record_count;
}
function get_player_record_list($maestro_id, $where_statement, $limit_count) {
function get_player_record_list($maestro_id, $start_date, $end_date, $player_name, $app_id, $limit_count) {
global $db_conn;
$where = array(
"BR.MaestroID = ?",
"BR.RecordDateTime >= DATE(?)",
"BR.RecordDateTime < DATE(?) + INTERVAL 1 DAY",
"BR.PlayerID = P.PlayerID",
"BR.AppID = A.AppID",
);
$types = "iss";
$params = array($maestro_id, $start_date, $end_date);
if (strlen($player_name) > 0) {
$where[] = "P.Name LIKE ?";
$types .= "s";
$params[] = "%" . $player_name . "%";
}
$range = get_subject_app_range($app_id);
if ($range !== null) {
$where[] = "BR.AppID BETWEEN ? AND ?";
$types .= "ii";
$params[] = $range[0];
$params[] = $range[1];
}
$query = "
SELECT BR.BestRecordID AS Id, DATE(BR.RecordDateTime) AS Date, TIME(BR.RecordDateTime) AS Time,
P.Name AS Name, A.KoreanName AS Subject, BR.BestRecord AS Record
SELECT BR.BestRecordID, DATE(BR.RecordDateTime), TIME(BR.RecordDateTime),
P.Name, A.KoreanName, BR.BestRecord
FROM best_record BR, player P, app A
WHERE BR.MaestroID = ? AND ".$where_statement."
ORDER BY BR.RecordDateTime DESC
";
WHERE " . implode(" AND ", $where) . "
ORDER BY BR.RecordDateTime DESC";
if($limit_count > 0)
$query = $query." LIMIT ".$limit_count;
// echo $query;
// return;
if ($limit_count > 0) {
$query .= " LIMIT ?";
$types .= "i";
$params[] = $limit_count;
}
$stmt = $db_conn->prepare($query);
$stmt->bind_param('i', $maestro_id);
$stmt->bind_param($types, ...$params);
$stmt->execute();
$stmt->bind_result($id, $date, $time, $player_name, $subject, $record);
$stmt->bind_result($id, $date, $time, $player_name_result, $subject, $record);
$record_array = array();
while($stmt->fetch()) {
$record_date['Id'] = $id;
$record_date['Date'] = $date;
$record_date['Time'] = $time;
$record_date['PlayerName'] = $player_name;
$record_date['Subject'] = $subject;
$record_date['Record'] = $record;
array_push($record_array, $record_date);
$record_date['Id'] = $id;
$record_date['Date'] = $date;
$record_date['Time'] = $time;
$record_date['PlayerName'] = $player_name_result;
$record_date['Subject'] = $subject;
$record_date['Record'] = $record;
array_push($record_array, $record_date);
}
$stmt->close();
return $record_array;
}
@@ -5,36 +5,15 @@ include "./../lib/send_reply_json.php";
include "./../lib/maestro_account_info.php";
include "./../setup/connect_db.php";
$maestro_id = $_POST["MaestroID"];
$maestro_id = (int)$_POST["MaestroID"];
$start_date = $_POST["StartDate"];
$end_date = $_POST["EndDate"];
$player_name_list = $_POST["PlayerNameList"];
$app_id = $_POST["AppID"];
$limit_count = $_POST["LimitCount"];
$player_name = trim($_POST["PlayerNameList"]);
$app_id = (int)$_POST["AppID"];
$limit_count = (int)$_POST["LimitCount"];
// echo "start_date : ".$start_date."\n";
// echo "end_date : ".$end_date."\n";
// echo "player_name_list : ".$player_name_list."\n";
// echo "app_id : ".$app_id."\n";
// echo "limit_count : ".$limit_count."\n";
// echo "\n";
$date_statement = makeDateStatement($start_date, $end_date);
$player_name_statement = makePlayerNameStatement($player_name_list);
$subject_statement = makeSubjectSentence($app_id);
// echo "maestro : ".$maestro_id."\n";
// echo "date : ".$date_statement."\n";
// echo "player_name : ".$player_name_statement."\n";
// echo "subject : ".$subject_statement."\n";
$where_statement = make_where_statement($date_statement, $player_name_statement, $subject_statement);
$recordTotalCount = get_player_record_total_count($maestro_id, $where_statement);
$replyJSON = get_player_record_list($maestro_id, $where_statement, $limit_count);
if($replyJSON.length === 0) {
send_error_message($replyJSON, "오늘의 랭킹 기록 없음");
$db_conn->close();
exit;
}
$recordTotalCount = get_player_record_total_count($maestro_id, $start_date, $end_date, $player_name, $app_id);
$replyJSON = get_player_record_list($maestro_id, $start_date, $end_date, $player_name, $app_id, $limit_count);
set_data("RecordTotalCount", $recordTotalCount);
set_data("RecordList", $replyJSON);
@@ -42,119 +21,52 @@ send_result_success();
exit;
function makeDateStatement($start_date, $end_date) {
return " '".$start_date."' < LS.ScoreDateTime AND LS.ScoreDateTime < '".$end_date."'";
// return " '2017-01-01' < LS.ScoreDateTime AND LS.ScoreDateTime < '".$end_date."'";
function get_subject_app_range($app_id) {
switch ($app_id) {
case 0: return null;
case 1000: return array(1, 8);
case 1001: return array(11, 18);
case 1002: return array(21, 30);
case 1003: return array(31, 40);
case 3000: return null;
default: return array($app_id, $app_id);
}
}
function makeSubjectSentence($app_id) {
if($app_id == 0)
return "";
else if($app_id == 3000)
return "";
$subject_statement = "";
if(is_subject_bundle_item($app_id)) {
switch($app_id) {
case 1000: // korean practice
$subject_statement = $subject_statement."A.AppID < 9";
break;
function get_player_record_total_count($maestro_id, $start_date, $end_date, $player_name, $app_id) {
global $db_conn;
case 1001: // english practice
$subject_statement = $subject_statement."10 < A.AppID AND A.AppID < 19";
break;
$where = array(
"LS.MaestroID = ?",
"LS.ScoreDateTime >= DATE(?)",
"LS.ScoreDateTime < DATE(?) + INTERVAL 1 DAY",
"LS.PlayerID = P.PlayerID",
);
$types = "iss";
$params = array($maestro_id, $start_date, $end_date);
case 1002: // korean test
$subject_statement = $subject_statement."20 < A.AppID AND A.AppID < 31";
break;
case 1003: // english test
$subject_statement = $subject_statement."30 < A.AppID AND A.AppID < 41";
break;
}
} else {
$subject_statement = $subject_statement."A.AppID = ".$app_id;
if (strlen($player_name) > 0) {
$where[] = "P.Name LIKE ?";
$types .= "s";
$params[] = "%" . $player_name . "%";
}
return $subject_statement." AND LS.AppID = A.AppID";
}
function makePlayerNameStatement($player_name_list) {
$player_name_statement = "LS.PlayerID = P.PlayerID";
if(strlen($player_name_list) > 0)
$player_name_statement = $player_name_statement." AND P.Name LIKE '%".$player_name_list."%'";
return $player_name_statement;
}
function is_subject_bundle_item($app_id) {
if($app_id >= 1000)
return true;
return false;
}
function make_where_statement($date_statement, $player_name_statement, $subject_statement) {
// add date condition
$where_statement = $date_statement;
if(strlen($where_statement) > 0 and strlen($player_name_statement) > 0)
$where_statement = $where_statement." AND ";
// add player name condition
$where_statement = $where_statement.$player_name_statement;
// add subject condition
if(strlen($where_statement) > 0 and strlen($subject_statement) > 0)
$where_statement = $where_statement." AND ";
$where_statement = $where_statement.$subject_statement;
return $where_statement;
}
/*
function get_player_record_count($maestro_id, $where_statement, $limit_count) {
global $db_conn;
$range = get_subject_app_range($app_id);
if ($range !== null) {
$where[] = "LS.AppID BETWEEN ? AND ?";
$types .= "ii";
$params[] = $range[0];
$params[] = $range[1];
}
$query = "
SELECT COUNT(*)
FROM license_score LS, player P
WHERE LS.MaestroID = ? AND ".$where_statement."
";
if($limit_count > 0)
$query = $query." LIMIT ".$limit_count;
// echo $query;
// return;
WHERE " . implode(" AND ", $where);
$stmt = $db_conn->prepare($query);
$stmt->bind_param("i", $maestro_id);
$stmt->execute();
$stmt->bind_result($record_count);
$stmt->fetch();
$stmt->close();
return $record_count;
}
*/
function get_player_record_total_count($maestro_id, $where_statement) {
global $db_conn;
$query = "
SELECT COUNT(*)
FROM license_score LS, player P
WHERE LS.MaestroID = ? AND ".$where_statement."
";
// echo $query;
// return;
$stmt = $db_conn->prepare($query);
$stmt->bind_param("i", $maestro_id);
$stmt->bind_param($types, ...$params);
$stmt->execute();
$stmt->bind_result($record_count);
$stmt->fetch();
@@ -163,38 +75,62 @@ function get_player_record_total_count($maestro_id, $where_statement) {
return $record_count;
}
function get_player_record_list($maestro_id, $where_statement, $limit_count) {
function get_player_record_list($maestro_id, $start_date, $end_date, $player_name, $app_id, $limit_count) {
global $db_conn;
$where = array(
"LS.MaestroID = ?",
"LS.ScoreDateTime >= DATE(?)",
"LS.ScoreDateTime < DATE(?) + INTERVAL 1 DAY",
"LS.PlayerID = P.PlayerID",
);
$types = "iss";
$params = array($maestro_id, $start_date, $end_date);
if (strlen($player_name) > 0) {
$where[] = "P.Name LIKE ?";
$types .= "s";
$params[] = "%" . $player_name . "%";
}
$range = get_subject_app_range($app_id);
if ($range !== null) {
$where[] = "LS.AppID BETWEEN ? AND ?";
$types .= "ii";
$params[] = $range[0];
$params[] = $range[1];
}
$query = "
SELECT LS.LicenseScoreID AS ID, DATE(LS.ScoreDateTime) AS Date, TIME(LS.ScoreDateTime) AS Time,
P.Name AS Name, LS.SubjectName AS Subject, LS.Score AS Record
SELECT LS.LicenseScoreID, DATE(LS.ScoreDateTime), TIME(LS.ScoreDateTime),
P.Name, LS.SubjectName, LS.Score
FROM license_score LS, player P
WHERE LS.MaestroID = ? AND ".$where_statement."
ORDER BY LS.ScoreDateTime DESC
";
WHERE " . implode(" AND ", $where) . "
ORDER BY LS.ScoreDateTime DESC";
if($limit_count > 0)
$query = $query." LIMIT ".$limit_count;
// echo $query;
// return;
if ($limit_count > 0) {
$query .= " LIMIT ?";
$types .= "i";
$params[] = $limit_count;
}
$stmt = $db_conn->prepare($query);
$stmt->bind_param('i', $maestro_id);
$stmt->bind_param($types, ...$params);
$stmt->execute();
$stmt->bind_result($id, $date, $time, $player_name, $subject, $record);
$stmt->bind_result($id, $date, $time, $player_name_result, $subject, $record);
$record_array = array();
while($stmt->fetch()) {
$record_date['Id'] = $id;
$record_date['Date'] = $date;
$record_date['Time'] = $time;
$record_date['PlayerName'] = $player_name;
$record_date['Subject'] = $subject;
$record_date['Record'] = $record;
array_push($record_array, $record_date);
$record_date['Id'] = $id;
$record_date['Date'] = $date;
$record_date['Time'] = $time;
$record_date['PlayerName'] = $player_name_result;
$record_date['Subject'] = $subject;
$record_date['Record'] = $record;
array_push($record_array, $record_date);
}
$stmt->close();
return $record_array;
}
@@ -5,37 +5,15 @@ include "./../lib/send_reply_json.php";
include "./../lib/maestro_account_info.php";
include "./../setup/connect_db.php";
$maestro_id = $_POST["MaestroID"];
$maestro_id = (int)$_POST["MaestroID"];
$start_date = $_POST["StartDate"];
$end_date = $_POST["EndDate"];
$player_name_list = $_POST["PlayerNameList"];
$app_id = $_POST["AppID"];
$limit_count = $_POST["LimitCount"];
// echo "start_date : ".$start_date."\n";
// echo "end_date : ".$end_date."\n";
// echo "player_name_list : ".$player_name_list."\n";
// echo "app_id : ".$app_id."\n";
// echo "limit_count : ".$limit_count."\n";
// echo "\n";
$player_name = trim($_POST["PlayerNameList"]);
$app_id = (int)$_POST["AppID"];
$limit_count = (int)$_POST["LimitCount"];
$date_statement = makeDateStatement($start_date, $end_date);
$player_name_statement = makePlayerNameStatement($player_name_list);
$subject_statement = makeSubjectSentence($app_id);
$where_statement = make_where_statement($date_statement, $player_name_statement, $subject_statement);
// echo "maestro : ".$maestro_id."\n";
// echo "date : ".$date_statement."\n";
// echo "player_name : ".$player_name_statement."\n";
// echo "subject : ".$subject_statement."\n";
// echo "where_statement : ".$where_statement."\n";
$recordTotalCount = get_player_record_total_count($maestro_id, $where_statement);
// echo "recordTotalCount : ".$recordTotalCount."\n";
$replyJSON = get_player_record_list($maestro_id, $where_statement, $limit_count);
if($replyJSON.length === 0) {
send_error_message($replyJSON, "오늘의 랭킹 기록 없음");
$db_conn->close();
exit;
}
$recordTotalCount = get_player_record_total_count($maestro_id, $start_date, $end_date, $player_name, $app_id);
$replyJSON = get_player_record_list($maestro_id, $start_date, $end_date, $player_name, $app_id, $limit_count);
set_data("RecordTotalCount", $recordTotalCount);
set_data("RecordList", $replyJSON);
@@ -43,84 +21,48 @@ send_result_success();
exit;
function get_player_record_total_count($maestro_id, $start_date, $end_date, $player_name, $app_id) {
global $db_conn;
$where = array(
"TE.MaestroID = ?",
"TE.RecordDateTime >= DATE(?)",
"TE.RecordDateTime < DATE(?) + INTERVAL 1 DAY",
"TE.PlayerID = P.PlayerID",
"TE.WritingID = WR.WritingID",
);
$types = "iss";
$params = array($maestro_id, $start_date, $end_date);
function makeDateStatement($start_date, $end_date) {
return " '".$start_date."' <= TE.RecordDateTime AND TE.RecordDateTime <= DATE_ADD('".$end_date."', INTERVAL 1 DAY)";
// return " '".$start_date."' <= TE.RecordDateTime AND TE.RecordDateTime <= '".$end_date."'";
// return " '2017-01-01' < TE.RecordDateTime";
// return " '2017-01-01' < TE.RecordDateTime AND TE.RecordDateTime < '".$end_date."'";
}
function makeSubjectSentence($app_id) {
if($app_id == 0)
return "";
$subject_statement = "";
if(is_subject_bundle_item($app_id)) {
switch($app_id) {
case 1004: // korean exam
$subject_statement = $subject_statement."WR.Language='korean'";
break;
case 1005: // english exam
$subject_statement = $subject_statement."WR.Language='english'";
break;
}
} else {
$subject_statement = $subject_statement."WR.WritingID=".$app_id;
if (strlen($player_name) > 0) {
$where[] = "P.Name LIKE ?";
$types .= "s";
$params[] = "%" . $player_name . "%";
}
return $subject_statement." AND TE.WritingID=WR.WritingID";
}
function makePlayerNameStatement($player_name_list) {
$player_name_statement = "TE.PlayerID=P.PlayerID";
if(strlen($player_name_list) > 0)
$player_name_statement = $player_name_statement." AND P.Name LIKE '%".$player_name_list."%'";
return $player_name_statement;
}
function is_subject_bundle_item($app_id) {
if($app_id >= 1000)
return true;
return false;
}
function make_where_statement($date_statement, $player_name_statement, $subject_statement) {
// add date condition
$where_statement = $date_statement;
if(strlen($where_statement) > 0 and strlen($player_name_statement) > 0)
$where_statement = $where_statement." AND ";
// add player name condition
$where_statement = $where_statement.$player_name_statement;
// add subject condition
if(strlen($where_statement) > 0 and strlen($subject_statement) > 0)
$where_statement = $where_statement." AND ";
$where_statement = $where_statement.$subject_statement;
return $where_statement;
}
function get_player_record_total_count($maestro_id, $where_statement) {
global $db_conn;
if ($app_id > 0) {
if ($app_id == 1004) {
$where[] = "WR.Language = ?";
$types .= "s";
$params[] = "korean";
} else if ($app_id == 1005) {
$where[] = "WR.Language = ?";
$types .= "s";
$params[] = "english";
} else {
$where[] = "WR.WritingID = ?";
$types .= "i";
$params[] = $app_id;
}
}
$query = "
SELECT COUNT(*)
FROM typing_exam_record TE, player P, writing WR
WHERE TE.MaestroID = ? AND ".$where_statement."
";
// echo $query;
// return;
WHERE " . implode(" AND ", $where);
$stmt = $db_conn->prepare($query);
$stmt->bind_param("i", $maestro_id);
$stmt->bind_param($types, ...$params);
$stmt->execute();
$stmt->bind_result($record_count);
$stmt->fetch();
@@ -129,38 +71,71 @@ function get_player_record_total_count($maestro_id, $where_statement) {
return $record_count;
}
function get_player_record_list($maestro_id, $where_statement, $limit_count) {
function get_player_record_list($maestro_id, $start_date, $end_date, $player_name, $app_id, $limit_count) {
global $db_conn;
$where = array(
"TE.MaestroID = ?",
"TE.RecordDateTime >= DATE(?)",
"TE.RecordDateTime < DATE(?) + INTERVAL 1 DAY",
"TE.PlayerID = P.PlayerID",
"TE.WritingID = WR.WritingID",
);
$types = "iss";
$params = array($maestro_id, $start_date, $end_date);
if (strlen($player_name) > 0) {
$where[] = "P.Name LIKE ?";
$types .= "s";
$params[] = "%" . $player_name . "%";
}
if ($app_id > 0) {
if ($app_id == 1004) {
$where[] = "WR.Language = ?";
$types .= "s";
$params[] = "korean";
} else if ($app_id == 1005) {
$where[] = "WR.Language = ?";
$types .= "s";
$params[] = "english";
} else {
$where[] = "WR.WritingID = ?";
$types .= "i";
$params[] = $app_id;
}
}
$query = "
SELECT TE.TypingExamRecordID as Id, DATE(TE.RecordDateTime) AS Date, TIME(TE.RecordDateTime) AS Time,
P.Name AS Name, WR.Name AS Subject, TE.Record AS Record
SELECT TE.TypingExamRecordID, DATE(TE.RecordDateTime), TIME(TE.RecordDateTime),
P.Name, WR.Name, TE.Record
FROM typing_exam_record TE, player P, writing WR
WHERE TE.MaestroID = ? AND TE.WritingID=WR.WritingID AND TE.PlayerID=P.PlayerID AND ".$where_statement."
ORDER BY TE.RecordDateTime DESC
";
WHERE " . implode(" AND ", $where) . "
ORDER BY TE.RecordDateTime DESC";
if($limit_count > 0)
$query = $query." LIMIT ".$limit_count;
// echo $query;
// return;
if ($limit_count > 0) {
$query .= " LIMIT ?";
$types .= "i";
$params[] = $limit_count;
}
$stmt = $db_conn->prepare($query);
$stmt->bind_param('i', $maestro_id);
$stmt->bind_param($types, ...$params);
$stmt->execute();
$stmt->bind_result($id, $date, $time, $player_name, $subject, $record);
$stmt->bind_result($id, $date, $time, $player_name_result, $subject, $record);
$record_array = array();
while($stmt->fetch()) {
$record_date['Id'] = $id;
$record_date['Date'] = $date;
$record_date['Time'] = $time;
$record_date['PlayerName'] = $player_name;
$record_date['Subject'] = $subject;
$record_date['Record'] = $record;
array_push($record_array, $record_date);
$record_date['Id'] = $id;
$record_date['Date'] = $date;
$record_date['Time'] = $time;
$record_date['PlayerName'] = $player_name_result;
$record_date['Subject'] = $subject;
$record_date['Record'] = $record;
array_push($record_array, $record_date);
}
$stmt->close();
return $record_array;
}
@@ -64,11 +64,12 @@ function get_best_record($maestro_id, $app_id, $player_id) {
$query = "
SELECT BestRecordID, BestRecord
FROM best_record
WHERE MaestroID = ? AND AppID = ? AND PlayerID = ? AND DATE(RecordDateTime) = DATE(NOW())
AND HOUR(RecordDateTime) = HOUR(NOW())
WHERE PlayerID = ? AND AppID = ? AND MaestroID = ?
AND RecordDateTime >= CAST(DATE_FORMAT(NOW(), '%Y-%m-%d %H:00:00') AS DATETIME)
AND RecordDateTime < CAST(DATE_FORMAT(NOW(), '%Y-%m-%d %H:00:00') AS DATETIME) + INTERVAL 1 HOUR
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("iii", $maestro_id, $app_id, $player_id);
$stmt->bind_param("iii", $player_id, $app_id, $maestro_id);
$stmt->execute();
$stmt->bind_result($prev_best_record_id, $prev_best_record);
$stmt->fetch();