Compare commits

...

10 Commits

Author SHA1 Message Date
jisangs 1ea9e64667 DB connect 내용에 port 번호 추가 2026-07-06 17:20:31 +09:00
jisangs 51b9a99f95 Time-based Blind SQL Injection 해킹 대비 2026-03-17 17:11:52 +09:00
jisangs a57fc28825 클로드 코드 초기화 2026-03-17 16:38:14 +09:00
jisangs 4ff50f465a 아이콘, 스크린샷 추가 2026-03-17 16:34:24 +09:00
jisangs 5decf048bf 오타 수정 2026-01-28 17:16:45 +09:00
jisangs 272cd484b8 개인정보 처리방침 화면 추가 2026-01-28 17:08:21 +09:00
jisangs d998aef91e 베타테스트 감사 자동 메일 제거 2025-08-31 15:57:13 +09:00
jisangs 0ae67bd63f 메일 발송 오류 사과 공지 작성 2025-08-31 11:13:32 +09:00
jisangs 12192aa5c7 AWS EC2 서버로 이전 (집 인터넷망 접속 끊김)
- EC2 서버 생성, Apache2 + PHP7.3 + MariaDB 환경 설정
- Gabia에서 chocomae.jinaju.com CNAME 정보 변경: EC2 서버 주소
2024-10-17 09:37:29 +09:00
jisangs 6b671f8881 학생 기록 삭제 기능 추가
- 기록 삭제시 highest_record 최고 기록도 갱신
2024-09-15 08:41:01 +09:00
21 changed files with 883 additions and 36 deletions
+3
View File
@@ -15,3 +15,6 @@ __pycache__/
# python compiled files # python compiled files
*.pyc *.pyc
# mac
.DS_Store
+97
View File
@@ -0,0 +1,97 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
ChocoMae (초코마에) is a Korean/English typing and mouse practice web application. It features gamified typing exercises (using Phaser 2.6.2), classroom management for teachers (Maestros), and a ranking/record system.
## Tech Stack
- **Frontend:** HTML5, JavaScript (ES5), jQuery 3.3.1, Phaser 2.6.2 game engine, Bootstrap
- **Backend:** PHP 7.x with MySQLi, Python (batch scripts)
- **Database:** MariaDB/MySQL (`chocomae` database)
- **Server:** Apache HTTP Server with SSL on AWS EC2 (Docker container)
## Development Commands
This project has **no Node.js/npm build step** for the main application. It runs directly on Apache + PHP.
**Phaser input plugin** (only if modifying `/src/util/phaser-input-master/`):
```sh
cd src/util/phaser-input-master
npm install # install grunt + TypeScript deps
grunt # build TypeScript → JS
```
**Database setup** (initialize schema):
```sh
mysql -u <user> -p chocomae < src/web/sql/make_db.sql
mysql -u <user> -p chocomae < src/web/sql/make_db_license_timer.sql
mysql -u <user> -p chocomae < src/web/sql/insert_app.sql
mysql -u <user> -p chocomae < src/web/sql/insert_active_app.sql
```
**Deployment** — push to `release` branch; the git post-receive hook automatically:
1. Checks out files to `/volume1/docker/chocomae/html/mouse_typing/`
2. Copies `NA_service_db_setting.php``service_db_setting.php` and replaces `localhost` with `mysql` (Docker hostname)
## Architecture
### Directory Structure
```
src/
├── game/ # Phaser game modules (client-side JS)
├── web/
│ ├── main/ # Entry point (index.html) + top-level HTML pages
│ ├── module/ # HTML fragments loaded dynamically via jQuery .load()
│ ├── js/ # Frontend JavaScript
│ ├── css/ # Stylesheets
│ ├── server/ # PHP backend endpoints
│ ├── admin/ # Admin panel HTML
│ ├── popup/ # Popup/modal HTML
│ └── sql/ # Database schema and seed files
├── php-cli/ # PHP batch scripts (run via cron)
└── util/ # Third-party utilities (phaser-input plugin source)
resources/ # Static assets: Bootstrap, jQuery, fonts, images
test/ # Manual test HTML/JS files (no automated test runner)
```
### Frontend Architecture
- **Entry point:** `src/web/main/index.html` — loads header/section/footer modules dynamically
- **Module loading:** jQuery `.load()` fetches HTML fragments from `src/web/module/`
- **State:** `sessionStorage` used for `maestroID` and game session data
- **Game engine:** All interactive games live in `src/game/` and use Phaser 2.6.2; each game directory has its own `main.js` with Phaser states (Boot, Load, Game, Result)
### Backend Architecture
PHP endpoints in `src/web/server/` are organized by domain:
- `player/` — CRUD for player accounts
- `record/` — Save/retrieve game results and rankings
- `maestro/` — Teacher classroom management (17 endpoints)
- `admin/` — Admin operations (11 endpoints)
- `license_timer/` — Subscription/license management
- `mail/` — Email sending via PHPMailer + Gmail SMTP
- `lib/` — Shared PHP libraries (`send_reply_json.php`, `db_maestro.php`, etc.)
- `setup/connect_db.php` — Database connection; reads from `service_db_setting.php`
All endpoints return JSON. UTF-8 encoding is set on all responses.
### Database Configuration
- **Development:** Edit `src/web/server/setup/NA_service_db_setting.php` (this is the template; `service_db_setting.php` is git-ignored and generated at deploy time)
- The deploy hook replaces `localhost``mysql` for Docker networking
### User Roles
| Role | Description |
|------|-------------|
| Player | End users (free or paid) who play games |
| Maestro | Teachers who manage groups of student players |
| Admin | System administrators with full access |
### Python Batch Scripts
Located in `src/web/server/python/chocomae/` and `src/php-cli/batch/` — handle scheduled tasks like sending expiration warning emails to unpaid/expiring Maestros. These are run via cron on the server.
Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

+62
View File
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta charset="UTF-8">
<title>개인정보 처리방침 | 초코마에</title>
<meta name="description" CONTENT="개인정보 처리방침 | 초코마에 ‧ Privacy policy | ChocoMae">
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-CMCJ6N2208"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-CMCJ6N2208');
</script>
<link rel="icon" href="/mouse_typing/resources/image/icon/favicon.ico" type="image/x-icon" />
<link rel="shortcut icon" href="/mouse_typing/resources/image/icon/favicon.ico" type="image/x-icon" />
<link href="../../../resources/font/open-iconic-master/font/css/open-iconic-bootstrap.css" rel="stylesheet">
<link href="../../../resources/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script type="text/javascript" src="./../../../resources/jquery/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="./../js/main.js"></script>
<script type="text/javascript">
$(document).ready(function() {
if(maestroID === -1)
$("#header").load("./../module/header.html");
else
$("#header").load("./../module/maestro_header.html");
$("#section").load("./../module/home_section_privacy_policy.html");
$("#footer").load("./../module/footer.html");
});
</script>
</head>
<body style="background-color: white">
<div id="wrapper">
<header id="header">
</header>
<section id="section">
</section>
<footer id="footer">
</footer>
</div>
</body>
</html>
+3
View File
@@ -13,6 +13,9 @@
</div> </div>
<div class="col-md-4 text-right my-auto"> <div class="col-md-4 text-right my-auto">
<a class="btn btn-lg btn-warning px-4" href="./../main/privacy_policy.html">
&nbsp;개인정보 처리방침
</a>
<a class="btn btn-lg btn-warning px-4" href="./../main/faq.html"> <a class="btn btn-lg btn-warning px-4" href="./../main/faq.html">
<span class="oi oi-bullhorn" title="icon bullhorn" aria-hidden="true"></span> <span class="oi oi-bullhorn" title="icon bullhorn" aria-hidden="true"></span>
&nbsp;FAQ &nbsp;FAQ
+6 -11
View File
@@ -1,20 +1,15 @@
<script> <script>
$(document).ready(function() { $(document).ready(function() {
/*
showErrorMessage( showErrorMessage(
'(0601일) 알림<br/>\ '(0128일) 알림<br/>\
* 한글 타자 입력 오류<br/>\ * 개인정보 처리방침 화면 추가<br/>\
5월 중순 이후로 갑자기 한글 타자 입력에 문제가 발생하고 있습니다. (Windows의 한글 입력기 영향으로 보입니다)<br/>\ 초코마에 서비스의 개인정보 처리방침 화면을 등록했습니다.<br/>\
한글 타자 입력이 잘 안될 때 <b>윈도우키를 두 번</b> 누르거나, <b>[Alt + Tab] 키를 두 번</b> 눌러보세요.<br/>\ 화면 제일 아래에 있는 푸터 오른쪽에 [개인정보 처리방침] 버튼을 추가했습니다.<br/>\
+ 더 많은 대응법을 확인해 보실 분은 아래 버튼을 눌러주세요.<br/>\ [개인정보 처리방침] 버튼을 클릭하시고 자세한 내용을 확인하세요.<br/>\
<br/>\ <br/>'
<a class="btn btn-danger" roll="button" target="_blank" \
href="https://jisangs.notion.site/dbd01211c8154581bed14c4a8c157ed2?pvs=4">\
초코마에-한글 타자 문제 해결 매뉴얼</a>'
, 'danger' , 'danger'
); );
*/
}); });
@@ -0,0 +1,221 @@
<div class="container my-3">
<div class="px-5 py-4 mb-3" style="background-color: #ffce54">
<h1 class="display-3 font-weight-bold" style="color: #630;">개인정보 처리방침</span></h1>
</div> <!-- jumbotron -->
<div class="card border-warning">
<h5 class="card-header text-danger font-weight-bold">제 1조(개인정보의 처리목적)</h5>
<div class="card-body">
<h5 class="card-title text-primary">다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 이외의 용도로 이용하지 않습니다.</h5>
<p class="card-text">
<ol>
<li>
홈페이지 회원 가입 및 관리
<ul>
<li>마에스트로 계정 생성시 본인 확인을 위한 목적으로 개인정보를 입력받습니다.</li>
<li>계정 생성 및 회원 정보 수정에 따른 정보 알림이 필요할 때, 이메일로 해당 내용을 전송합니다.</li>
</ul>
</li>
<li>
민원 사무 처리
<ul>
<li>서비스와 관련된 의견 접수, 처리, 수정 요청시 이메일로 내용을 주고 받습니다.</li>
</ul>
</li>
</ol>
</p>
</div>
</div>
<br/>
<div class="card border-warning">
<h5 class="card-header text-danger font-weight-bold">제 2조(개인정보의 처리 및 보유기간)</h5>
<div class="card-body">
<h5 class="card-title text-primary">개인정보는 아래와 같이 처리됩니다.</h5>
<p class="card-text">
<ol>
<li>
수집하는 개인정보 대상
<ul>
<li>이메일 계정: 마에스트로 회원 가입시 이메일 계정을 입력받습니다.</li>
</ul>
</li>
<li>
개인정보 수집 및 변경
<ul>
<li>서비스 이용을 위해 마에스트로 계정을 생성할 때 이메일로 개인정보를 받습니다.</li>
<li>마에스트로 계정 생성 후, 본인이 희망할 경우 관리자 화면에서 개인정보를 변경할 수 있습니다.</li>
</ul>
</li>
<li>
개인정보 보유기간
<ul>
<li>회원 탈퇴 전까지 개인정보가 유효합니다.</li>
</ul>
</li>
</ol>
</p>
</div>
</div>
<br/>
<div class="card border-warning">
<h5 class="card-header text-danger font-weight-bold">제 3조(개인정보의 제3자 제공)</h5>
<div class="card-body">
<h5 class="card-title text-primary">초코마에 서비스에서 수집한 개인정보는 제3자에 제공되지 않고 있습니다.</h5>
<p class="card-text">
<ul>
<li>
다음의 경우를 제외하고는 정보 주체의 사전 동의 없이는 제3자에게 제공하지 않습니다.
<ol>
<li>정보주체로부터 별도의 동의를 받는 경우</li>
<li>법률에 특별한 규정이 있는 경우</li>
<li>정보주체 또는 법정대리인의 의사표시를 할 수 없는 상태에 있거나 주소불명 등으로 사전동의를 받을 수 없는 경우로서 명백히 정보주체 또는 제3자의 급박한 생명, 신체, 재산의 이익을 위하여 필요하다고 인정되는 경우</li>
<li>통계작성 및 학술연구 등의 목적을 위하여 필요한 경우로서 특정 개인을 알아 볼 수 없는 형태로 개인정보를 제공하는 경우</li>
<li>개인정보를 목적 외의 용도로 이용하거나 이를 제3자에게 제공하지 아니하면 다른 법률에서 정하는 소관 업무를 수행할 수 없는 경우</li>
<li>조약, 그 밖의 국제협정의 이행을 위하여 외국정보 또는 국제기구에 제공하기 위하여 필요한 경우</li>
<li>범죄의 수사와 공소의 제기 및 유지를 위하여 필요한 경우</li>
<li>법원의 재판업무 수행을 위하여 필요한 경우</li>
<li>형 및 감호, 보호처분의 집행을 위하여 필요한 경우</li>
</ol>
</li>
</ul>
</p>
</div>
</div>
<br/>
<div class="card border-warning">
<h5 class="card-header text-danger font-weight-bold">제 4조(개인정보처리의 위탁)</h5>
<div class="card-body">
<h5 class="card-title text-primary">초코마에 서비스에서는 수집한 개인정보 처리를 위탁하고 있지 않습니다.</h5>
</div>
</div>
<br/>
<div class="card border-warning">
<h5 class="card-header text-danger font-weight-bold">제 5조(정보주체의 권리, 의무 및 행사방법)</h5>
<div class="card-body">
<h5 class="card-title text-primary">개인정보에 대한 처리를 요청하시면, 아래와 같은 작업을 수행합니다.</h5>
<p class="card-text">
<ol>
<li>
정보주체의 요구
<ul>
<li>개인정보 열람요구</li>
<li>정정요구</li>
<li>삭제요구</li>
</ul>
</li>
<li>초코마에 관리자 이메일 계정으로 요청 내용을 작성하고 보내주시면, 지체 없이 조치하겠습니다.</li>
<li>법정대리인이나 위임을 받은 자 등 대리인을 통해서도 요청할 수 있습니다. 이 경우 위임장을 함께 제출하셔야 합니다.</li>
</ol>
</p>
</div>
</div>
<br/>
<div class="card border-warning">
<h5 class="card-header text-danger font-weight-bold">제 6조(처리하는 개인정보 항목)</h5>
<div class="card-body">
<h5 class="card-title text-primary">다음과 같은 개인정보를 수집하고 처리하고 있습니다.</h5>
<p class="card-text">
<ol>
<li>
수집하는 개인정보 대상
<ul>
<li>이메일 계정</li>
</ul>
</li>
<li>
개인정보 처리 내용
<ul>
<li>서비스 이용을 위해 마에스트로 계정을 생성할 때 이메일로 계정 정보를 전송합니다.</li>
<li>마에스트로 계정 정보를 변경했을 때 이메일로 변경 내용을 전송합니다.</li>
<li>마에스트로 계정 결제 요청 및 결제 완료시 이메일로 결제 진행 안내 및 결제 완료 내용을 전송합니다.</li>
</ul>
</li>
</ol>
</p>
</div>
</div>
<br/>
<div class="card border-warning">
<h5 class="card-header text-danger font-weight-bold">제 7조(개인정보의 파기)</h5>
<div class="card-body">
<h5 class="card-title text-primary">계정 삭제를 요청하시면 해당 계정의 개인정보를 파기합니다.</h5>
<p class="card-text">
<ol>
<li>
파기 방법
<ul>
<li>DB에 저장된 개인정보를 삭제합니다.</li>
</ul>
</li>
</ol>
</p>
</div>
</div>
<br/>
<div class="card border-warning">
<h5 class="card-header text-danger font-weight-bold">제 8조(개인정보 보호책임자 및 열람청구)</h5>
<div class="card-body">
<h5 class="card-title text-primary">개인정보 처리에 관한 업무를 개발자 본인이 책임지고 수행하고 있습니다.</h5>
<p class="card-text">
<ol>
<li>
개인정보 보호 담당자
<ul>
<li>지나주 개인사업자</li>
<li>담당자명: 박지상</li>
<li>이메일: support@jinaju.com</li>
</ul>
</li>
</ol>
</p>
</div>
</div>
<br/>
<div class="card border-warning">
<h5 class="card-header text-danger font-weight-bold">제 9조(만14세 미만 아동의 개인정보 보호)</h5>
<div class="card-body">
<h5 class="card-title text-primary">만14세 미만 아동의 개인정보를 별도로 수집하지 않고 있습니다.</h5>
<p class="card-text">
<ol>
<li>초코마에 관리자 마에스트로 계정이 학생 계정을 생성할 때, 학생의 개인정보를 수집하지 않습니다.</li>
<li>만14세 미만 아동의 학생 계정 생성도 관리자인 마에스트로 계정이 자율적으로 처리하고 관리하는 시스템입니다.</li>
</ol>
</p>
</div>
</div>
<br/>
<div class="card border-warning">
<h5 class="card-header text-danger font-weight-bold">제 10조(권익침해 구제방법)</h5>
<div class="card-body">
<h5 class="card-title text-primary">아래 기관에서 개인정보 침해에 대한 피해주게, 상담 등을 문의하실 수 있습니다.</h5>
<p class="card-text">
<ol>
<li>개인정보 침해신고센터 : (국번없이) 118 (privacy.kisa.or.kr)</li>
<li>개인정보 분쟁조정위원회 : (국번없이) 118 (privacy.kisa.or.kr)</li>
<li>대검찰청 사이버범죄수사단 : 02-3480-3571 (cybercid@spo.go.kr)</li>
<li>경찰청 사이버테러대응센터 : 1566-0112 (www.netan.go.kr)</li>
</ol>
</p>
</div>
</div>
<br/>
<div class="card border-warning">
<h5 class="card-header text-danger font-weight-bold">제 11조(개인정보 처리방침 변경)</h5>
<div class="card-body">
<h5 class="card-title text-primary">이 개인정보 처리방침은 2026. 01. 28. 부터 적용됩니다</h5>
</div>
</div>
<br/>
</div>
+59 -6
View File
@@ -182,7 +182,9 @@ function makeTypingAppSubjectList(appList) {
for(var i = 0; i < appList.length; i++) { for(var i = 0; i < appList.length; i++) {
var appData = appList[i]; var appData = appList[i];
if(appData.appID < 50) if(appData.appID == 200) // 긴 글 시험
continue;
else if(appData.appID < 50) // 손가락 연습, 단어/문장 연습
continue; continue;
else if(appData.koreanName == "") else if(appData.koreanName == "")
continue; continue;
@@ -269,7 +271,7 @@ function getPlayerRecordList(limitCount) {
+ "&AppID=" + appID + "&LimitCount=" + limitCount, + "&AppID=" + appID + "&LimitCount=" + limitCount,
(function(jsonData) { (function(jsonData) {
// console.log(jsonData); console.log(jsonData);
showRecordCount(jsonData); showRecordCount(jsonData);
makeRecordTableContent(jsonData["RecordList"]); makeRecordTableContent(jsonData["RecordList"]);
}), }),
@@ -303,43 +305,92 @@ function makeRecordTableContent(recordList) {
// console.log(recordList); // console.log(recordList);
for(var i = 0; i < recordList.length; i++) { for(var i = 0; i < recordList.length; i++) {
var record = recordList[i]; var record = recordList[i];
addRecordListItem(record.Date, record.Time, record.PlayerName, record.Subject, record.Record); addRecordListItem(record.Id, record.Date, record.Time, record.PlayerName, record.Subject, record.Record);
} }
} }
function showNoResultContent() { function showNoResultContent() {
$("#record_table_content").empty();
$("#record_table_content").append('\ $("#record_table_content").append('\
<tr>\ <tr>\
<th scope="row" class="table-warning text-center" colspan="5">데이터가 없습니다.</th>\ <th scope="row" class="table-warning text-center" colspan="6">데이터가 없습니다.</th>\
</tr>\ </tr>\
'); ');
} }
function addRecordListItem(date, time, name, subject, record) { function addRecordListItem(id, date, time, name, subject, record) {
if(record >= 0) { if(record >= 0) {
$("#record_table_content").append('\ $("#record_table_content").append('\
<tr>\ <tr>\
<td class="text-center d-none">' + id + '</td>\
<th scope="row" class="text-center">' + date + '</th>\ <th scope="row" class="text-center">' + date + '</th>\
<td class="text-center">' + time + '</td>\ <td class="text-center">' + time + '</td>\
<td class="text-center">' + name + '</td>\ <td class="text-center">' + name + '</td>\
<td class="text-center">' + subject + '</td>\ <td class="text-center">' + subject + '</td>\
<td class="text-right">' + record + '</td>\ <td class="text-right">' + record + '</td>\
<td class="text-center"><button class="btn btn-danger" type="button" onClick="deleteRecord(this)"> X </button></td>\
</tr>\ </tr>\
'); ');
} else { } else {
var disqualificationRecord = -1 * record; var disqualificationRecord = -1 * record;
$("#record_table_content").append('\ $("#record_table_content").append('\
<tr>\ <tr>\
<td class="text-center d-none">' + id + '</td>\
<th scope="row" class="text-center">' + date + '</th>\ <th scope="row" class="text-center">' + date + '</th>\
<td class="text-center">' + time + '</td>\ <td class="text-center">' + time + '</td>\
<td class="text-center">' + name + '</td>\ <td class="text-center">' + name + '</td>\
<td class="text-center">' + subject + '</td>\ <td class="text-center">' + subject + '</td>\
<td class="text-right text-danger">(아깝) ' + disqualificationRecord + '</td>\ <td class="text-right text-danger">(아깝) ' + disqualificationRecord + '</td>\
<td class="text-center"><button class="btn btn-danger" type="button" onClick="deleteRecord(this)"> X </button></td>\
</tr>\ </tr>\
'); ');
} }
} }
function deleteRecord(target) {
console.log(target);
var id = $(target).parent().siblings().eq(0).text();
var date = $(target).parent().siblings().eq(1).text();
var time = $(target).parent().siblings().eq(2).text();
var name = $(target).parent().siblings().eq(3).text();
var subject = $(target).parent().siblings().eq(4).text();
var record = $(target).parent().siblings().eq(5).text();
var message = "선택한 기록을 삭제하시겠습니까?\n"
+ "(경고: 기록을 삭제한 후에는 되살릴 수 없습니다)\n\n"
+ "* 날짜: " + date + "\n"
+ "* 시간: " + time + "\n"
+ "* 이름: " + name + "\n"
+ "* 과목: " + subject + "\n"
+ "* 기록: " + record;
if (confirm(message)) {
console.log("delete record: " + id);
var appID = $(":radio[name='subject_group']:checked").val();
if(typeof(appID) == "undefined") {
showErrorMessage("검색할 과목을 선택하세요.");
return false;
}
sendRequestServer( //url, sendParams, onSuccess, onFail, isDebugMode);
"./../server/record/delete_record.php",
"MaestroID=" + maestroID
+ "&AppID=" + appID
+ "&BestRecordID=" + id,
(function(jsonData) {
console.log(jsonData);
requestPlayerRecordListAll();
}),
(function(errorMessage, errorCode) {
showErrorMessage(errorMessage, "error");
})
);
}
}
</script> </script>
@@ -491,17 +542,19 @@ function addRecordListItem(date, time, name, subject, record) {
<table class="table table-striped" id="dataTable"> <table class="table table-striped" id="dataTable">
<thead class="thead-dark"> <thead class="thead-dark">
<tr> <tr>
<th scope="col" class="text-center d-none">RecordID</th>
<th scope="col" class="text-center">날짜</th> <th scope="col" class="text-center">날짜</th>
<th scope="col" class="text-center">시간</th> <th scope="col" class="text-center">시간</th>
<th scope="col" class="text-center">이름</th> <th scope="col" class="text-center">이름</th>
<th scope="col" class="text-center">과목</th> <th scope="col" class="text-center">과목</th>
<th scope="col" class="text-center">점수</th> <th scope="col" class="text-center">점수</th>
<th scope="col" class="text-center">기록 삭제</th>
</tr> </tr>
</thead> </thead>
<tbody id="record_table_content"> <tbody id="record_table_content">
<tr> <tr>
<th scope="row" class="table-warning text-center" colspan="5">데이터가 없습니다.</th> <th scope="row" class="table-warning text-center" colspan="6">데이터가 없습니다.</th>
</tr> </tr>
</tbody> </tbody>
+5 -3
View File
@@ -9,11 +9,11 @@ if ($has_file) {
class DBConnector class DBConnector
{ {
private $mysqli, $stmt, $query, $paramTypeList, $paramValueArray; private $mysqli, $stmt, $query, $paramTypeList, $paramValueArray;
private $db_host, $db_user, $db_passwd, $db_name; private $db_host, $db_user, $db_passwd, $db_name, $db_port;
function __construct() function __construct()
{ {
global $db_host, $db_user, $db_passwd, $db_name; global $db_host, $db_user, $db_passwd, $db_name, $db_port;
$this->stmt = null; $this->stmt = null;
$this->paramTypeList = ""; $this->paramTypeList = "";
@@ -24,11 +24,13 @@ class DBConnector
$this->db_user = $db_user; $this->db_user = $db_user;
$this->db_passwd = $db_passwd; $this->db_passwd = $db_passwd;
$this->db_name = $db_name; $this->db_name = $db_name;
$this->db_port = isset($db_port) ? $db_port : 3306;
} else { } else {
$this->db_host = "localhost"; $this->db_host = "localhost";
$this->db_user = "chocomae"; $this->db_user = "chocomae";
$this->db_passwd = "sEobMPuJ2A8KTfwU"; $this->db_passwd = "sEobMPuJ2A8KTfwU";
$this->db_name = "chocomae"; $this->db_name = "chocomae";
$this->db_port = 3306;
} }
} }
@@ -39,7 +41,7 @@ class DBConnector
public function connectDB() public function connectDB()
{ {
// connect db // connect db
$this->mysqli = new mysqli($this->db_host, $this->db_user, $this->db_passwd, $this->db_name); $this->mysqli = new mysqli($this->db_host, $this->db_user, $this->db_passwd, $this->db_name, $this->db_port);
// if ($this->mysqli->connect_error) { // if ($this->mysqli->connect_error) {
if (mysqli_connect_errno()) { if (mysqli_connect_errno()) {
return false; return false;
+28
View File
@@ -0,0 +1,28 @@
# SQL Injection 방어 - Apache 레벨 WAF
# Time-based Blind SQL Injection 공격 패턴 차단
<IfModule mod_rewrite.c>
RewriteEngine On
# SLEEP(), BENCHMARK() 등 시간 지연 함수 차단 (Time-based Blind SQLi 핵심 패턴)
RewriteCond %{QUERY_STRING} (sleep|benchmark|waitfor\s+delay|pg_sleep) [NC]
RewriteRule .* - [F,L]
RewriteCond %{REQUEST_URI} (sleep|benchmark|waitfor\s+delay|pg_sleep) [NC]
RewriteRule .* - [F,L]
# POST body의 SQLi 패턴은 mod_security로 처리 (아래 참조)
</IfModule>
# PHP 에러 메시지 노출 방지
<IfModule mod_php7.c>
php_flag display_errors Off
php_flag log_errors On
</IfModule>
<IfModule mod_php.c>
php_flag display_errors Off
php_flag log_errors On
</IfModule>
# 서버 정보 숨기기
ServerSignature Off
+386
View File
@@ -0,0 +1,386 @@
<?php
header('Content-Type: application/json');
$maestro_id = $_POST["MaestroID"];
$selected_app_id = $_POST["AppID"];
$best_record_id = $_POST["BestRecordID"];
include "./../lib/send_reply_json.php";
include "./../setup/connect_db.php";
include "./../lib/app_highest_record.php";
include "./../lib/util_app.php";
function sendSuccessReplyForTest() {
global $db_conn;
global $maestro_id;
global $selected_app_id;
global $best_record_id;
set_data("maestro_id", $maestro_id);
set_data("selected_app_id", $selected_app_id);
set_data("best_record_id", $best_record_id);
send_result_success();
$db_conn->close();
exit;
}
//sendSuccessReplyForTest();
// 앱 종류에 따라
if($selected_app_id == 3000) { // 자격증 타이머
// 기록 삭제 : license_score
$deletedRecordCount = delete_license_score($best_record_id);
if($deletedRecordCount < 1) {
sendErrorMessageAndExit("자격증 타이머 기록 삭제 실패");
}
} else if($selected_app_id == 1004 || $selected_app_id == 1005) { // 긴글 (시험)
$record_info_array = get_typing_exam_record_info($best_record_id);
$player_id = $record_info_array["PlayerID"];
$writing_id = $record_info_array["WritingID"];
if($player_id === null || $writing_id === null) {
sendErrorMessageAndExit("긴글 (시험) 기록 오류");
}
// 기록 삭제 : typing_exam_record
$deletedRecordCount = delete_typing_exam_record($best_record_id);
if($deletedRecordCount < 1) {
sendErrorMessageAndExit("긴글 (시험) 기록 삭제 실패");
}
// typing_exam_highest_record 업데이트
updateTypingExamHighestRecord($maestro_id, $player_id, $writing_id);
}
else { // 일반 앱
$record_info_array = get_best_record_info($best_record_id);
$player_id = $record_info_array["PlayerID"];
$app_id = $record_info_array["AppID"];
if($player_id === null || $app_id === null) {
sendErrorMessageAndExit("기록 오류");
}
// 기록 삭제 : best_record
$deletedRecordCount = delete_best_record($best_record_id);
if($deletedRecordCount < 1) {
sendErrorMessageAndExit("기록 삭제 실패");
}
// app_highest_record 업데이트
updateAppHighestRecord($maestro_id, $player_id, $app_id);
}
set_data("message", "기록 삭제 성공");
send_result_success();
$db_conn->close();
exit;
function sendErrorMessageAndExit($message) {
global $db_conn;
set_error_message($message);
send_result_fail();
$db_conn->close();
exit;
}
function updateTypingExamHighestRecord($maestro_id, $player_id, $writing_id) {
global $db_conn;
$record_info_array = get_typing_exam_best_record_info($maestro_id, $player_id, $writing_id);
$record = $record_info_array["Record"];
$record_date_time = $record_info_array["RecordDateTime"];
if ($record === null || $record_date_time === null) {
delete_typing_exam_highest_record($maestro_id, $player_id, $writing_id);
return;
}
$highest_record_id = get_typing_exam_highest_record_id($maestro_id, $player_id, $writing_id);
if ($highest_record_id !== null && $highest_record_id > 0) {
update_typing_exam_highest_record($highest_record_id, $record, $record_date_time);
} else {
insert_new_typing_exam_highest_record($maestro_id, $player_id, $writing_id, $record, $record_date_time);
}
}
function updateAppHighestRecord($maestro_id, $player_id, $app_id) {
$record_info_array = get_best_record_record_info($maestro_id, $player_id, $app_id);
$best_record = $record_info_array["BestRecord"];
$record_date_time = $record_info_array["RecordDateTime"];
if ($best_record === null || $record_date_time === null) {
delete_app_highest_record($maestro_id, $player_id, $app_id);
return;
}
$highest_record_id = get_app_highest_record_id($maestro_id, $player_id, $app_id);
if ($highest_record_id !== null && $highest_record_id > 0) {
update_app_highest_record($highest_record_id, $best_record, $record_date_time);
} else {
insert_new_app_highest_record($maestro_id, $player_id, $app_id, $best_record, $record_date_time);
}
}
function delete_license_score($best_record_id) {
global $db_conn;
$query = "
DELETE FROM license_score
WHERE LicenseScoreID = ?
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("i", $best_record_id);
$result = $stmt->execute();
$stmt->close();
return $result;
}
function get_typing_exam_record_info($best_record_id) {
global $db_conn;
$query = "
SELECT PlayerID, WritingID
FROM typing_exam_record
WHERE TypingExamRecordID = ?
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("i", $best_record_id);
$stmt->execute();
$stmt->bind_result($player_id, $writing_id);
$stmt->fetch();
$stmt->close();
return array("PlayerID" => $player_id, "WritingID" => $writing_id);
}
function delete_typing_exam_record($best_record_id) {
global $db_conn;
$query = "
DELETE FROM typing_exam_record
WHERE TypingExamRecordID = ?
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("i", $best_record_id);
$result = $stmt->execute();
$stmt->close();
return $result;
}
function get_typing_exam_best_record_info($maestro_id, $player_id, $writing_id) {
global $db_conn;
$query = "
SELECT Record, RecordDateTime
FROM typing_exam_record
WHERE MaestroID = ? AND PlayerID = ? AND WritingID = ? AND Record > 0
ORDER BY Record DESC
LIMIT 1
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("iii", $maestro_id, $player_id, $writing_id);
$stmt->execute();
$stmt->bind_result($record, $record_date_time);
$stmt->fetch();
$stmt->close();
return array("Record" => $record, "RecordDateTime" => $record_date_time);
}
function get_typing_exam_highest_record_id($maestro_id, $player_id, $writing_id) {
global $db_conn;
$query = "
SELECT TypingExamHighestRecordID
FROM typing_exam_highest_record
WHERE MaestroID = ? AND PlayerID = ? AND WritingID = ? AND HighestRecord > 0
ORDER BY HighestRecord DESC
LIMIT 1
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("iii", $maestro_id, $player_id, $writing_id);
$stmt->execute();
$stmt->bind_result($new_best_record_id);
$stmt->fetch();
$stmt->close();
return $new_best_record_id;
}
function delete_typing_exam_highest_record($maestro_id, $player_id, $writing_id)
{
global $db_conn;
$query = "
DELETE FROM typing_exam_highest_record
WHERE MaestroID = ? AND PlayerID = ? AND WritingID = ?
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("iii", $maestro_id, $player_id, $writing_id);
$result = $stmt->execute();
$stmt->close();
return $result;
}
function update_typing_exam_highest_record($highest_record_id, $record, $record_date_time) {
global $db_conn;
$query = "
UPDATE typing_exam_highest_record
SET HighestRecord = ?, RecordDateTime = ?
WHERE TypingExamHighestRecordID = ?
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("dsi", $record, $record_date_time, $highest_record_id);
$result = $stmt->execute();
$stmt->close();
return $result;
}
function insert_new_typing_exam_highest_record($maestro_id, $player_id, $writing_id, $record, $record_date_time) {
global $db_conn;
$query = "
INSERT INTO typing_exam_highest_record (MaestroID, PlayerID, WritingID, HighestRecord, RecordDateTime)
VALUES (?, ?, ?, ?, ?)
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("iiids", $maestro_id, $player_id, $writing_id, $record, $record_date_time);
$result = $stmt->execute();
$stmt->close();
return $result;
}
function get_best_record_info($best_record_id) {
global $db_conn;
$query = "
SELECT PlayerID, AppID
FROM best_record
WHERE BestRecordID = ?
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("i", $best_record_id);
$stmt->execute();
$stmt->bind_result($player_id, $app_id);
$stmt->fetch();
$stmt->close();
return array("PlayerID" => $player_id, "AppID" => $app_id);
}
function delete_best_record($best_record_id) {
global $db_conn;
$query = "
DELETE FROM best_record
WHERE BestRecordID = ?
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("i", $best_record_id);
$result = $stmt->execute();
$stmt->close();
return $result;
}
function get_best_record_record_info($maestro_id, $player_id, $app_id) {
global $db_conn;
$orderStatement = is_highest_record_prefer_app($app_id) ? "ORDER BY BestRecord DESC" : "ORDER BY BestRecord ASC";
$query = "
SELECT BestRecord, RecordDateTime
FROM best_record
WHERE MaestroID = ? AND PlayerID = ? AND AppID = ?
".$orderStatement."
LIMIT 1
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("iii", $maestro_id, $player_id, $app_id);
$stmt->execute();
$stmt->bind_result($best_record, $record_date_time);
$stmt->fetch();
$stmt->close();
return array("BestRecord" => $best_record, "RecordDateTime" => $record_date_time);
}
function get_app_highest_record_id($maestro_id, $player_id, $app_id)
{
global $db_conn;
$orderStatement = is_highest_record_prefer_app($app_id) ? "ORDER BY HighestRecord DESC" : "ORDER BY HighestRecord ASC";
$query = "
SELECT AppHighestRecordID
FROM app_highest_record
WHERE MaestroID = ? AND PlayerID = ? AND AppID = ?
".$orderStatement."
LIMIT 1
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("iii", $maestro_id, $player_id, $app_id);
$stmt->execute();
$stmt->bind_result($highest_record_id);
$stmt->fetch();
$stmt->close();
return $highest_record_id;
}
function delete_app_highest_record($maestro_id, $player_id, $app_id)
{
global $db_conn;
$query = "
DELETE FROM app_highest_record
WHERE MaestroID = ? AND PlayerID = ? AND AppID = ?
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("iii", $maestro_id, $player_id, $app_id);
$result = $stmt->execute();
$stmt->close();
return $result;
}
function update_app_highest_record($highest_record_id, $record, $record_date_time) {
global $db_conn;
$query = "
UPDATE app_highest_record
SET HighestRecord = ?, RecordDateTime = ?
WHERE AppHighestRecordID = ?
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param("dsi", $record, $record_date_time, $highest_record_id);
$result = $stmt->execute();
$stmt->close();
return $result;
}
function insert_new_app_highest_record($maestro_id, $player_id, $app_id, $record, $record_date_time) {
global $db_conn;
$query = "
INSERT INTO app_highest_record (MaestroID, PlayerID, AppID, HighestRecord, RecordDateTime)
VALUES (?, ?, ?, ?, ?)
";
$stmt = $db_conn->prepare($query);
$stmt->bind_param('iiids', $maestro_id, $player_id, $app_id, $record, $record_date_time);
$result = $stmt->execute();
$stmt->close();
return $result;
}
?>
@@ -165,7 +165,7 @@ function get_player_record_list($maestro_id, $where_statement, $limit_count) {
global $db_conn; global $db_conn;
$query = " $query = "
SELECT DATE(BR.RecordDateTime) AS Date, TIME(BR.RecordDateTime) AS Time, 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 P.Name AS Name, A.KoreanName AS Subject, BR.BestRecord AS Record
FROM best_record BR, player P, app A FROM best_record BR, player P, app A
WHERE BR.MaestroID = ? AND ".$where_statement." WHERE BR.MaestroID = ? AND ".$where_statement."
@@ -181,10 +181,11 @@ function get_player_record_list($maestro_id, $where_statement, $limit_count) {
$stmt = $db_conn->prepare($query); $stmt = $db_conn->prepare($query);
$stmt->bind_param('i', $maestro_id); $stmt->bind_param('i', $maestro_id);
$stmt->execute(); $stmt->execute();
$stmt->bind_result($date, $time, $player_name, $subject, $record); $stmt->bind_result($id, $date, $time, $player_name, $subject, $record);
$record_array = array(); $record_array = array();
while($stmt->fetch()) { while($stmt->fetch()) {
$record_date['Id'] = $id;
$record_date['Date'] = $date; $record_date['Date'] = $date;
$record_date['Time'] = $time; $record_date['Time'] = $time;
$record_date['PlayerName'] = $player_name; $record_date['PlayerName'] = $player_name;
@@ -167,7 +167,7 @@ function get_player_record_list($maestro_id, $where_statement, $limit_count) {
global $db_conn; global $db_conn;
$query = " $query = "
SELECT DATE(LS.ScoreDateTime) AS Date, TIME(LS.ScoreDateTime) AS Time, 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 P.Name AS Name, LS.SubjectName AS Subject, LS.Score AS Record
FROM license_score LS, player P FROM license_score LS, player P
WHERE LS.MaestroID = ? AND ".$where_statement." WHERE LS.MaestroID = ? AND ".$where_statement."
@@ -183,10 +183,11 @@ function get_player_record_list($maestro_id, $where_statement, $limit_count) {
$stmt = $db_conn->prepare($query); $stmt = $db_conn->prepare($query);
$stmt->bind_param('i', $maestro_id); $stmt->bind_param('i', $maestro_id);
$stmt->execute(); $stmt->execute();
$stmt->bind_result($date, $time, $player_name, $subject, $record); $stmt->bind_result($id, $date, $time, $player_name, $subject, $record);
$record_array = array(); $record_array = array();
while($stmt->fetch()) { while($stmt->fetch()) {
$record_date['Id'] = $id;
$record_date['Date'] = $date; $record_date['Date'] = $date;
$record_date['Time'] = $time; $record_date['Time'] = $time;
$record_date['PlayerName'] = $player_name; $record_date['PlayerName'] = $player_name;
@@ -133,7 +133,7 @@ function get_player_record_list($maestro_id, $where_statement, $limit_count) {
global $db_conn; global $db_conn;
$query = " $query = "
SELECT DATE(TE.RecordDateTime) AS Date, TIME(TE.RecordDateTime) AS Time, 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 P.Name AS Name, WR.Name AS Subject, TE.Record AS Record
FROM typing_exam_record TE, player P, writing WR 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." WHERE TE.MaestroID = ? AND TE.WritingID=WR.WritingID AND TE.PlayerID=P.PlayerID AND ".$where_statement."
@@ -149,10 +149,11 @@ function get_player_record_list($maestro_id, $where_statement, $limit_count) {
$stmt = $db_conn->prepare($query); $stmt = $db_conn->prepare($query);
$stmt->bind_param('i', $maestro_id); $stmt->bind_param('i', $maestro_id);
$stmt->execute(); $stmt->execute();
$stmt->bind_result($date, $time, $player_name, $subject, $record); $stmt->bind_result($id, $date, $time, $player_name, $subject, $record);
$record_array = array(); $record_array = array();
while($stmt->fetch()) { while($stmt->fetch()) {
$record_date['Id'] = $id;
$record_date['Date'] = $date; $record_date['Date'] = $date;
$record_date['Time'] = $time; $record_date['Time'] = $time;
$record_date['PlayerName'] = $player_name; $record_date['PlayerName'] = $player_name;
@@ -16,21 +16,16 @@ $prev_best_record_id = -1;
$prev_best_record = 0; $prev_best_record = 0;
$prev_best_record_id = get_best_record($maestro_id, $app_id, $player_id); $prev_best_record_id = get_best_record($maestro_id, $app_id, $player_id);
echo "id : ".$prev_best_record_id." ".$prev_best_record;
if($prev_best_record_id === null || $prev_best_record_id < 0) { if($prev_best_record_id === null || $prev_best_record_id < 0) {
echo "insert";
insert_best_record($maestro_id, $app_id, $player_id, $record); insert_best_record($maestro_id, $app_id, $player_id, $record);
} else { } else {
echo $record." ".$prev_best_record;
if(is_highest_record_prefer_app($app_id)) { if(is_highest_record_prefer_app($app_id)) {
if($record > $prev_best_record) { if($record > $prev_best_record) {
echo "update";
update_best_record($prev_best_record_id, $record); update_best_record($prev_best_record_id, $record);
} }
} else { } else {
if($record < $prev_best_record) { if($record < $prev_best_record) {
echo "update";
update_best_record($prev_best_record_id, $record); update_best_record($prev_best_record_id, $record);
} }
} }
@@ -38,9 +33,6 @@ if($prev_best_record_id === null || $prev_best_record_id < 0) {
// highest record is the highest record // highest record is the highest record
$app_highest_record = get_app_highest_record($maestro_id, $app_id, $player_id); $app_highest_record = get_app_highest_record($maestro_id, $app_id, $player_id);
echo "\napp_highest_record : $app_highest_record";
// echo "\nrecord : $record";
if($app_highest_record == null) { if($app_highest_record == null) {
insert_app_highest_record($maestro_id, $app_id, $player_id, $record); insert_app_highest_record($maestro_id, $app_id, $player_id, $record);
// echo "\ninsert_app_highest_record : $record"; // echo "\ninsert_app_highest_record : $record";
@@ -4,5 +4,6 @@ $db_host = "localhost";
$db_user = "chocomae"; $db_user = "chocomae";
$db_passwd = "sEobMPuJ2A8KTfwU"; $db_passwd = "sEobMPuJ2A8KTfwU";
$db_name = "chocomae"; $db_name = "chocomae";
$db_port = 3306;
?> ?>
+3 -2
View File
@@ -10,12 +10,13 @@ if($hasServiceDBSetting == true) {
$db_passwd = "Fr12nds95"; $db_passwd = "Fr12nds95";
$db_name = "jisangs"; $db_name = "jisangs";
} }
if (!isset($db_port)) $db_port = 3306;
// connect db // connect db
$db_conn = new mysqli($db_host, $db_user, $db_passwd, $db_name); $db_conn = new mysqli($db_host, $db_user, $db_passwd, $db_name, $db_port);
if ($db_conn->connect_error) { if ($db_conn->connect_error) {
set_error_message("Failed to connect DB: ".$db_conn->connect_error); set_error_message("DB connection failed.");
send_result_fail(); send_result_fail();
exit; exit;
} else { } else {