Compare commits

...

14 Commits

Author SHA1 Message Date
jisangs be5b2ad6e6 스테이지용 변수 수정: stage-chocoadmin -> chocoadmin-stage 2026-07-09 11:16:10 +09:00
jisangs d97d13c219 도커 컨테이너 이름 수정: stage-chocomae-stage-chocomae -> chocomae-stage 2026-07-09 09:48:34 +09:00
jisangs aeef3b63cb Stage 서버에 deploy 할 때 index.html 파일을 /var/www/html 폴더에 복사하는 내용 추가 2026-07-06 22:25:34 +09:00
jisangs e17b1f0f1f Stage 서버 deploy 파일 추가 2026-07-06 22:16:34 +09:00
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
25 changed files with 957 additions and 36 deletions
+3
View File
@@ -15,3 +15,6 @@ __pycache__/
# python compiled files
*.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.
+28
View File
@@ -0,0 +1,28 @@
FROM php:7.3-apache
RUN apt-get update && apt-get install -y libonig-dev \
&& docker-php-ext-install mysqli mbstring \
&& rm -rf /var/lib/apt/lists/*
RUN a2enmod rewrite
RUN { \
echo '<Directory /var/www/html>'; \
echo ' AllowOverride All'; \
echo '</Directory>'; \
echo ''; \
echo '<FilesMatch "\.(php|html|htm|inc)$">'; \
echo ' SetHandler application/x-httpd-php'; \
echo '</FilesMatch>'; \
} > /etc/apache2/conf-available/chocomae.conf \
&& a2enconf chocomae
ENV TZ=Asia/Seoul
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN printf '#!/bin/sh\ncp /var/www/html/mouse_typing/index.html /var/www/html/index.html\nexec "$@"\n' \
> /usr/local/bin/entrypoint.sh \
&& chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["apache2-foreground"]
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
BASE_DIR=/volume1/docker/service/jisangs/chocomae-stage
CHOCOMAE_DIR=$BASE_DIR/chocomae
echo "=== [1/3] git pull ==="
git -C $CHOCOMAE_DIR pull
if [ $? -ne 0 ]; then
echo "git pull 실패. 배포 중단."
exit 1
fi
echo "=== [2/3] service_db_setting.php 복사 ==="
cp $BASE_DIR/service_db_setting.php $CHOCOMAE_DIR/src/web/server/setup/service_db_setting.php
if [ $? -ne 0 ]; then
echo "service_db_setting.php 복사 실패. 배포 중단."
exit 1
fi
echo "=== [3/3] docker compose up ==="
docker compose -f $BASE_DIR/docker-compose.stage.yml up -d
if [ $? -ne 0 ]; then
echo "docker compose up 실패. 배포 중단."
exit 1
fi
echo "=== 배포 완료 ==="
+19
View File
@@ -0,0 +1,19 @@
version: '3'
services:
chocomae-stage:
build: .
image: chocomae-stage:latest
container_name: chocomae-stage
hostname: chocomae-stage
volumes:
- ./chocomae:/var/www/html/mouse_typing
environment:
- TZ=Asia/Seoul
networks:
- proxy-network
restart: unless-stopped
networks:
proxy-network:
external: true
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 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">
<span class="oi oi-bullhorn" title="icon bullhorn" aria-hidden="true"></span>
&nbsp;FAQ
+6 -11
View File
@@ -1,20 +1,15 @@
<script>
$(document).ready(function() {
/*
showErrorMessage(
'(0601일) 알림<br/>\
* 한글 타자 입력 오류<br/>\
5월 중순 이후로 갑자기 한글 타자 입력에 문제가 발생하고 있습니다. (Windows의 한글 입력기 영향으로 보입니다)<br/>\
한글 타자 입력이 잘 안될 때 <b>윈도우키를 두 번</b> 누르거나, <b>[Alt + Tab] 키를 두 번</b> 눌러보세요.<br/>\
+ 더 많은 대응법을 확인해 보실 분은 아래 버튼을 눌러주세요.<br/>\
<br/>\
<a class="btn btn-danger" roll="button" target="_blank" \
href="https://jisangs.notion.site/dbd01211c8154581bed14c4a8c157ed2?pvs=4">\
초코마에-한글 타자 문제 해결 매뉴얼</a>'
'(0128일) 알림<br/>\
* 개인정보 처리방침 화면 추가<br/>\
초코마에 서비스의 개인정보 처리방침 화면을 등록했습니다.<br/>\
화면 제일 아래에 있는 푸터 오른쪽에 [개인정보 처리방침] 버튼을 추가했습니다.<br/>\
[개인정보 처리방침] 버튼을 클릭하시고 자세한 내용을 확인하세요.<br/>\
<br/>'
, '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++) {
var appData = appList[i];
if(appData.appID < 50)
if(appData.appID == 200) // 긴 글 시험
continue;
else if(appData.appID < 50) // 손가락 연습, 단어/문장 연습
continue;
else if(appData.koreanName == "")
continue;
@@ -269,7 +271,7 @@ function getPlayerRecordList(limitCount) {
+ "&AppID=" + appID + "&LimitCount=" + limitCount,
(function(jsonData) {
// console.log(jsonData);
console.log(jsonData);
showRecordCount(jsonData);
makeRecordTableContent(jsonData["RecordList"]);
}),
@@ -303,43 +305,92 @@ function makeRecordTableContent(recordList) {
// console.log(recordList);
for(var i = 0; i < recordList.length; 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() {
$("#record_table_content").empty();
$("#record_table_content").append('\
<tr>\
<th scope="row" class="table-warning text-center" colspan="5">데이터가 없습니다.</th>\
<th scope="row" class="table-warning text-center" colspan="6">데이터가 없습니다.</th>\
</tr>\
');
}
function addRecordListItem(date, time, name, subject, record) {
function addRecordListItem(id, date, time, name, subject, record) {
if(record >= 0) {
$("#record_table_content").append('\
<tr>\
<td class="text-center d-none">' + id + '</td>\
<th scope="row" class="text-center">' + date + '</th>\
<td class="text-center">' + time + '</td>\
<td class="text-center">' + name + '</td>\
<td class="text-center">' + subject + '</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>\
');
} else {
var disqualificationRecord = -1 * record;
$("#record_table_content").append('\
<tr>\
<td class="text-center d-none">' + id + '</td>\
<th scope="row" class="text-center">' + date + '</th>\
<td class="text-center">' + time + '</td>\
<td class="text-center">' + name + '</td>\
<td class="text-center">' + subject + '</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>\
');
}
}
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>
@@ -491,17 +542,19 @@ function addRecordListItem(date, time, name, subject, record) {
<table class="table table-striped" id="dataTable">
<thead class="thead-dark">
<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>
</tr>
</thead>
<tbody id="record_table_content">
<tr>
<th scope="row" class="table-warning text-center" colspan="5">데이터가 없습니다.</th>
<th scope="row" class="table-warning text-center" colspan="6">데이터가 없습니다.</th>
</tr>
</tbody>
+5 -3
View File
@@ -9,11 +9,11 @@ if ($has_file) {
class DBConnector
{
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()
{
global $db_host, $db_user, $db_passwd, $db_name;
global $db_host, $db_user, $db_passwd, $db_name, $db_port;
$this->stmt = null;
$this->paramTypeList = "";
@@ -24,11 +24,13 @@ class DBConnector
$this->db_user = $db_user;
$this->db_passwd = $db_passwd;
$this->db_name = $db_name;
$this->db_port = isset($db_port) ? $db_port : 3306;
} else {
$this->db_host = "localhost";
$this->db_user = "chocomae";
$this->db_passwd = "sEobMPuJ2A8KTfwU";
$this->db_name = "chocomae";
$this->db_port = 3306;
}
}
@@ -39,7 +41,7 @@ class DBConnector
public function connectDB()
{
// 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 (mysqli_connect_errno()) {
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;
$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
FROM best_record BR, player P, app A
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->bind_param('i', $maestro_id);
$stmt->execute();
$stmt->bind_result($date, $time, $player_name, $subject, $record);
$stmt->bind_result($id, $date, $time, $player_name, $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;
@@ -167,7 +167,7 @@ function get_player_record_list($maestro_id, $where_statement, $limit_count) {
global $db_conn;
$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
FROM license_score LS, player P
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->bind_param('i', $maestro_id);
$stmt->execute();
$stmt->bind_result($date, $time, $player_name, $subject, $record);
$stmt->bind_result($id, $date, $time, $player_name, $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;
@@ -133,7 +133,7 @@ function get_player_record_list($maestro_id, $where_statement, $limit_count) {
global $db_conn;
$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
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."
@@ -149,10 +149,11 @@ function get_player_record_list($maestro_id, $where_statement, $limit_count) {
$stmt = $db_conn->prepare($query);
$stmt->bind_param('i', $maestro_id);
$stmt->execute();
$stmt->bind_result($date, $time, $player_name, $subject, $record);
$stmt->bind_result($id, $date, $time, $player_name, $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;
@@ -16,21 +16,16 @@ $prev_best_record_id = -1;
$prev_best_record = 0;
$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) {
echo "insert";
insert_best_record($maestro_id, $app_id, $player_id, $record);
} else {
echo $record." ".$prev_best_record;
if(is_highest_record_prefer_app($app_id)) {
if($record > $prev_best_record) {
echo "update";
update_best_record($prev_best_record_id, $record);
}
} else {
if($record < $prev_best_record) {
echo "update";
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
$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) {
insert_app_highest_record($maestro_id, $app_id, $player_id, $record);
// echo "\ninsert_app_highest_record : $record";
@@ -4,5 +4,6 @@ $db_host = "localhost";
$db_user = "chocomae";
$db_passwd = "sEobMPuJ2A8KTfwU";
$db_name = "chocomae";
$db_port = 3306;
?>
+3 -2
View File
@@ -10,12 +10,13 @@ if($hasServiceDBSetting == true) {
$db_passwd = "Fr12nds95";
$db_name = "jisangs";
}
if (!isset($db_port)) $db_port = 3306;
// 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) {
set_error_message("Failed to connect DB: ".$db_conn->connect_error);
set_error_message("DB connection failed.");
send_result_fail();
exit;
} else {