refactor: Phase 1 - 긴급 보안 및 중복 제거
## 🚨 보안 강화 - 하드코딩된 비밀번호를 환경변수로 전환 - .env.example 생성 및 보안 가이드 추가 - docker-compose.yml 환경변수 적용 - README.md에서 실제 비밀번호 제거 ## 🗑️ 중복 제거 - synology_deployment/ 디렉토리 제거 (268MB) - synology_deployment*.tar.gz 아카이브 제거 (234MB) - 총 502MB의 중복 파일 삭제 ## 🧹 백업 파일 정리 - *.backup 파일 제거 (10개) - *복사본* 파일 제거 - *이전* 파일 제거 - json(백업)/ 디렉토리 제거 ## 📋 .gitignore 업데이트 - 백업 파일 패턴 추가 - 보안 파일 제외 (.env, *.pem, *.key) - 임시 파일 제외 (*.tmp, *.new) - 빌드 아티팩트 제외 (*.tar.gz) ## 📚 문서화 - docs/ 디렉토리 구조 생성 - 리팩토링 분석 및 계획 문서 작성 - 코딩 스타일 가이드 작성 - 개발 환경 설정 가이드 작성 - 시스템 아키텍처 문서 작성 ## 변경된 파일 - .env.example (신규) - .gitignore (업데이트) - docker-compose.yml (환경변수 적용) - README.md (보안 정보 제거) - docs/* (신규 문서 7개) ## 보안 개선 효과 ✅ 비밀번호 노출 위험 제거 ✅ Git 히스토리에서 민감 정보 분리 ✅ 환경별 설정 분리 가능 ✅ 배포 보안 강화 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
686
docs/guides/CODING_STYLE.md
Normal file
686
docs/guides/CODING_STYLE.md
Normal file
@@ -0,0 +1,686 @@
|
||||
# 코딩 스타일 가이드
|
||||
|
||||
> TK-FB-Project 코드 작성 규칙 및 베스트 프랙티스
|
||||
|
||||
## 📚 목차
|
||||
|
||||
1. [일반 원칙](#일반-원칙)
|
||||
2. [JavaScript/Node.js](#javascriptnodejs)
|
||||
3. [HTML/CSS](#htmlcss)
|
||||
4. [SQL](#sql)
|
||||
5. [Git 커밋 메시지](#git-커밋-메시지)
|
||||
6. [파일 및 디렉토리 구조](#파일-및-디렉토리-구조)
|
||||
|
||||
---
|
||||
|
||||
## 일반 원칙
|
||||
|
||||
### 1. 가독성 우선
|
||||
```javascript
|
||||
// ❌ 나쁜 예
|
||||
const x = a.filter(i => i.active).map(i => ({...i, name: i.n}));
|
||||
|
||||
// ✅ 좋은 예
|
||||
const activeWorkers = workers
|
||||
.filter(worker => worker.is_active)
|
||||
.map(worker => ({
|
||||
...worker,
|
||||
name: worker.name
|
||||
}));
|
||||
```
|
||||
|
||||
### 2. 명확한 네이밍
|
||||
```javascript
|
||||
// ❌ 나쁜 예
|
||||
const d = new Date();
|
||||
const arr = [];
|
||||
const temp = getUserData();
|
||||
|
||||
// ✅ 좋은 예
|
||||
const currentDate = new Date();
|
||||
const activeWorkers = [];
|
||||
const userData = getUserData();
|
||||
```
|
||||
|
||||
### 3. 단일 책임 원칙
|
||||
```javascript
|
||||
// ❌ 나쁜 예 - 하나의 함수가 너무 많은 일을 함
|
||||
async function processReport(data) {
|
||||
// 검증
|
||||
if (!data.worker_id) throw new Error('Invalid');
|
||||
// DB 저장
|
||||
await db.query('INSERT INTO ...');
|
||||
// 이메일 발송
|
||||
await sendEmail();
|
||||
// 알림 전송
|
||||
await sendNotification();
|
||||
}
|
||||
|
||||
// ✅ 좋은 예 - 책임 분리
|
||||
async function processReport(data) {
|
||||
validateReport(data);
|
||||
const report = await saveReport(data);
|
||||
await notifyReportCreation(report);
|
||||
return report;
|
||||
}
|
||||
|
||||
function validateReport(data) {
|
||||
if (!data.worker_id) {
|
||||
throw new ValidationError('작업자를 선택해주세요');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveReport(data) {
|
||||
return await reportRepository.create(data);
|
||||
}
|
||||
|
||||
async function notifyReportCreation(report) {
|
||||
await emailService.send(report);
|
||||
await notificationService.send(report);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. DRY (Don't Repeat Yourself)
|
||||
```javascript
|
||||
// ❌ 나쁜 예 - 중복 코드
|
||||
if (!req.user || !['admin', 'system'].includes(req.user.access_level)) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
|
||||
// ✅ 좋은 예 - 재사용 가능한 미들웨어
|
||||
const requireRole = (...roles) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user || !roles.includes(req.user.access_level)) {
|
||||
throw new ForbiddenError();
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
// 사용
|
||||
router.get('/admin', requireRole('admin', 'system'), getAdminData);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JavaScript/Node.js
|
||||
|
||||
### 변수 선언
|
||||
|
||||
```javascript
|
||||
// ✅ const 우선, 재할당 필요시 let, var 사용 금지
|
||||
const API_URL = 'http://api.example.com';
|
||||
let currentPage = 1;
|
||||
|
||||
// ❌ var 사용 금지
|
||||
var x = 10; // NO!
|
||||
```
|
||||
|
||||
### 함수 작성
|
||||
|
||||
```javascript
|
||||
// ✅ 화살표 함수 사용 (콜백, 간단한 함수)
|
||||
const double = (n) => n * 2;
|
||||
const sum = (a, b) => a + b;
|
||||
|
||||
// ✅ 일반 함수 (메서드, 복잡한 로직)
|
||||
function calculateTotalHours(reports) {
|
||||
let total = 0;
|
||||
for (const report of reports) {
|
||||
total += report.work_hours;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// ✅ async/await 사용 (Promise보다 선호)
|
||||
async function fetchUserData(userId) {
|
||||
try {
|
||||
const user = await userModel.findById(userId);
|
||||
const reports = await reportModel.findByUser(userId);
|
||||
return { user, reports };
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch user data', { userId, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 에러 처리
|
||||
|
||||
```javascript
|
||||
// ❌ 나쁜 예
|
||||
try {
|
||||
await saveData();
|
||||
} catch (error) {
|
||||
console.log(error); // 단순 로그만
|
||||
}
|
||||
|
||||
// ✅ 좋은 예
|
||||
try {
|
||||
await saveData(data);
|
||||
} catch (error) {
|
||||
logger.error('데이터 저장 실패', {
|
||||
data,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw new AppError('데이터 저장에 실패했습니다', 500);
|
||||
}
|
||||
```
|
||||
|
||||
### 객체 및 배열
|
||||
|
||||
```javascript
|
||||
// ✅ 구조 분해 할당
|
||||
const { name, email, phone } = user;
|
||||
const [first, second, ...rest] = items;
|
||||
|
||||
// ✅ 스프레드 연산자
|
||||
const newUser = { ...user, is_active: true };
|
||||
const allItems = [...items1, ...items2];
|
||||
|
||||
// ✅ 단축 속성
|
||||
const name = 'John';
|
||||
const age = 30;
|
||||
const user = { name, age }; // { name: name, age: age } 대신
|
||||
```
|
||||
|
||||
### 비동기 처리
|
||||
|
||||
```javascript
|
||||
// ❌ 콜백 지옥
|
||||
getData(function(a) {
|
||||
getMoreData(a, function(b) {
|
||||
getMoreData(b, function(c) {
|
||||
console.log(c);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ async/await
|
||||
async function processData() {
|
||||
const a = await getData();
|
||||
const b = await getMoreData(a);
|
||||
const c = await getMoreData(b);
|
||||
return c;
|
||||
}
|
||||
|
||||
// ✅ 병렬 처리가 가능한 경우
|
||||
const [users, projects, reports] = await Promise.all([
|
||||
fetchUsers(),
|
||||
fetchProjects(),
|
||||
fetchReports()
|
||||
]);
|
||||
```
|
||||
|
||||
### 조건문
|
||||
|
||||
```javascript
|
||||
// ✅ Early Return 패턴
|
||||
function processUser(user) {
|
||||
if (!user) {
|
||||
throw new ValidationError('User is required');
|
||||
}
|
||||
|
||||
if (!user.is_active) {
|
||||
throw new ValidationError('User is not active');
|
||||
}
|
||||
|
||||
// 메인 로직
|
||||
return processActiveUser(user);
|
||||
}
|
||||
|
||||
// ❌ 중첩된 조건문
|
||||
function processUser(user) {
|
||||
if (user) {
|
||||
if (user.is_active) {
|
||||
// 메인 로직
|
||||
return processActiveUser(user);
|
||||
} else {
|
||||
throw new Error('Not active');
|
||||
}
|
||||
} else {
|
||||
throw new Error('No user');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 주석
|
||||
|
||||
```javascript
|
||||
// ✅ JSDoc 사용
|
||||
/**
|
||||
* 작업 보고서를 생성합니다
|
||||
* @param {Object} reportData - 보고서 데이터
|
||||
* @param {number} reportData.worker_id - 작업자 ID
|
||||
* @param {string} reportData.work_content - 작업 내용
|
||||
* @param {number} userId - 생성하는 사용자 ID
|
||||
* @returns {Promise<Object>} 생성된 보고서
|
||||
* @throws {ValidationError} 검증 실패 시
|
||||
*/
|
||||
async function createReport(reportData, userId) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ 복잡한 로직 설명
|
||||
// NOTE: MySQL 8.0에서는 GROUP BY 동작이 다르므로 명시적으로 컬럼 지정
|
||||
const query = `
|
||||
SELECT worker_id, COUNT(*) as count
|
||||
FROM reports
|
||||
GROUP BY worker_id
|
||||
ORDER BY count DESC
|
||||
`;
|
||||
|
||||
// ⚠️ TODO, FIXME 등 명확히 표시
|
||||
// TODO: 캐싱 로직 추가 필요
|
||||
// FIXME: 날짜 범위 검증 개선 필요
|
||||
// HACK: 임시 해결책, 나중에 리팩토링 필요
|
||||
```
|
||||
|
||||
### 모듈 구조
|
||||
|
||||
```javascript
|
||||
// ✅ 명확한 import/export
|
||||
// 파일 상단에 모든 import
|
||||
const express = require('express');
|
||||
const { ValidationError } = require('../utils/errors');
|
||||
const reportService = require('../services/reportService');
|
||||
|
||||
// 함수 정의
|
||||
function createReport(req, res, next) {
|
||||
// ...
|
||||
}
|
||||
|
||||
function getReports(req, res, next) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 파일 하단에 export
|
||||
module.exports = {
|
||||
createReport,
|
||||
getReports
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTML/CSS
|
||||
|
||||
### HTML
|
||||
|
||||
```html
|
||||
<!-- ✅ 시맨틱 태그 사용 -->
|
||||
<header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">홈</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<article>
|
||||
<h1>제목</h1>
|
||||
<p>내용</p>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© 2025 TK-FB</p>
|
||||
</footer>
|
||||
|
||||
<!-- ✅ 들여쓰기 2칸 -->
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
내용
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ 속성 순서 -->
|
||||
<!-- 1. class 2. id 3. data-* 4. 기타 -->
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
id="submit-btn"
|
||||
data-action="submit"
|
||||
type="submit"
|
||||
disabled
|
||||
>
|
||||
제출
|
||||
</button>
|
||||
```
|
||||
|
||||
### CSS
|
||||
|
||||
```css
|
||||
/* ✅ 클래스 네이밍: BEM 방식 */
|
||||
.block {}
|
||||
.block__element {}
|
||||
.block--modifier {}
|
||||
|
||||
/* 예시 */
|
||||
.card {}
|
||||
.card__header {}
|
||||
.card__body {}
|
||||
.card__footer {}
|
||||
.card--large {}
|
||||
.card--primary {}
|
||||
|
||||
/* ✅ 속성 순서 */
|
||||
.element {
|
||||
/* 1. Positioning */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
|
||||
/* 2. Box Model */
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #000;
|
||||
|
||||
/* 3. Typography */
|
||||
font-family: Arial;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
|
||||
/* 4. Visual */
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
|
||||
/* 5. Other */
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* ✅ CSS 변수 사용 */
|
||||
:root {
|
||||
--color-primary: #007bff;
|
||||
--color-success: #28a745;
|
||||
--spacing-md: 1rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: var(--color-primary);
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* ✅ 중첩 최소화 (3단계 이하) */
|
||||
.nav {}
|
||||
.nav__item {}
|
||||
.nav__link {}
|
||||
|
||||
/* ❌ 과도한 중첩 */
|
||||
.nav ul li a span {} /* NO! */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SQL
|
||||
|
||||
### 쿼리 작성
|
||||
|
||||
```sql
|
||||
-- ✅ 대문자 키워드, 명시적 컬럼 지정
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
created_at
|
||||
FROM users
|
||||
WHERE is_active = 1
|
||||
AND role = 'admin'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10;
|
||||
|
||||
-- ❌ SELECT * 사용 금지
|
||||
SELECT * FROM users; -- NO!
|
||||
|
||||
-- ✅ 조인 명시적 작성
|
||||
SELECT
|
||||
r.id,
|
||||
r.work_content,
|
||||
w.name AS worker_name,
|
||||
p.name AS project_name
|
||||
FROM daily_work_reports r
|
||||
INNER JOIN workers w ON r.worker_id = w.id
|
||||
LEFT JOIN projects p ON r.project_id = p.id
|
||||
WHERE r.report_date BETWEEN ? AND ?;
|
||||
|
||||
-- ✅ 파라미터 바인딩 사용
|
||||
const query = 'SELECT * FROM users WHERE id = ? AND email = ?';
|
||||
const [rows] = await db.query(query, [userId, email]);
|
||||
|
||||
-- ❌ 문자열 연결 금지 (SQL Injection 위험)
|
||||
const query = `SELECT * FROM users WHERE email = '${email}'`; -- NO!
|
||||
```
|
||||
|
||||
### 테이블/컬럼 네이밍
|
||||
|
||||
```sql
|
||||
-- ✅ 스네이크 케이스
|
||||
CREATE TABLE daily_work_reports (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
worker_id INT NOT NULL,
|
||||
report_date DATE NOT NULL,
|
||||
work_content TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ✅ 복수형 테이블명
|
||||
users, workers, projects, reports
|
||||
|
||||
-- ✅ 외래키 명확히
|
||||
worker_id, project_id (테이블명_id 형식)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git 커밋 메시지
|
||||
|
||||
### 형식
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
### Type
|
||||
- `feat`: 새로운 기능
|
||||
- `fix`: 버그 수정
|
||||
- `refactor`: 리팩토링
|
||||
- `style`: 코드 포맷팅
|
||||
- `docs`: 문서 수정
|
||||
- `test`: 테스트 추가/수정
|
||||
- `chore`: 빌드, 설정 변경
|
||||
|
||||
### 예시
|
||||
|
||||
```bash
|
||||
# ✅ 좋은 커밋 메시지
|
||||
feat(report): 작업 보고서 엑셀 내보내기 기능 추가
|
||||
|
||||
사용자가 작업 보고서를 엑셀 파일로 내보낼 수 있도록 기능 추가
|
||||
- xlsx 라이브러리 사용
|
||||
- 날짜 범위 선택 가능
|
||||
- 필터링 옵션 제공
|
||||
|
||||
Closes #123
|
||||
|
||||
# ✅ 리팩토링
|
||||
refactor(api): index.js를 여러 파일로 분리
|
||||
|
||||
- config/database.js: DB 연결 설정
|
||||
- config/cors.js: CORS 설정
|
||||
- middlewares/auth.js: 인증 미들웨어
|
||||
- controllers/userController.js: 사용자 관리 API
|
||||
|
||||
index.js 파일 크기: 889줄 → 95줄
|
||||
|
||||
# ✅ 버그 수정
|
||||
fix(calendar): 주말 날짜 선택 시 오류 수정
|
||||
|
||||
주말 날짜 클릭 시 TypeError 발생하는 문제 해결
|
||||
Date 객체 null 체크 추가
|
||||
|
||||
Fixes #456
|
||||
|
||||
# ❌ 나쁜 커밋 메시지
|
||||
update
|
||||
fix bug
|
||||
asdf
|
||||
ㅁㄴㅇㄹ
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 파일 및 디렉토리 구조
|
||||
|
||||
### 백엔드
|
||||
|
||||
```
|
||||
api.hyungi.net/
|
||||
├── index.js # 진입점 (간결하게)
|
||||
├── config/ # 설정 파일
|
||||
│ ├── database.js
|
||||
│ ├── cors.js
|
||||
│ └── routes.js
|
||||
├── controllers/ # 라우트 핸들러
|
||||
│ ├── userController.js
|
||||
│ └── reportController.js
|
||||
├── services/ # 비즈니스 로직
|
||||
│ ├── userService.js
|
||||
│ └── reportService.js
|
||||
├── models/ # 데이터 모델
|
||||
│ ├── User.js
|
||||
│ └── Report.js
|
||||
├── middlewares/ # 미들웨어
|
||||
│ ├── auth.js
|
||||
│ └── errorHandler.js
|
||||
├── utils/ # 유틸리티
|
||||
│ ├── errors.js
|
||||
│ └── logger.js
|
||||
└── tests/ # 테스트
|
||||
├── unit/
|
||||
└── integration/
|
||||
```
|
||||
|
||||
### 프론트엔드
|
||||
|
||||
```
|
||||
web-ui/
|
||||
├── js/
|
||||
│ ├── modules/ # 모듈화된 코드
|
||||
│ │ ├── common/
|
||||
│ │ │ ├── api-client.js
|
||||
│ │ │ └── utils.js
|
||||
│ │ ├── calendar/
|
||||
│ │ │ ├── CalendarView.js
|
||||
│ │ │ └── CalendarAPI.js
|
||||
│ │ └── dashboard/
|
||||
│ └── pages/ # 페이지별 스크립트
|
||||
├── css/
|
||||
│ ├── base/
|
||||
│ ├── components/
|
||||
│ ├── layouts/
|
||||
│ └── pages/
|
||||
└── pages/ # HTML 파일
|
||||
```
|
||||
|
||||
### 파일 네이밍
|
||||
|
||||
```javascript
|
||||
// ✅ 케이스 규칙
|
||||
// 파일명: kebab-case
|
||||
user-controller.js
|
||||
daily-work-report.js
|
||||
|
||||
// 클래스/컴포넌트: PascalCase
|
||||
UserController.js
|
||||
CalendarView.js
|
||||
|
||||
// 일반 함수/변수: camelCase
|
||||
const getUserData = () => {};
|
||||
const reportService = {};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 코드 리뷰 체크리스트
|
||||
|
||||
### 제출 전 확인
|
||||
- [ ] 코드가 스타일 가이드를 따르는가?
|
||||
- [ ] 주석이 적절한가?
|
||||
- [ ] 테스트가 작성되었는가?
|
||||
- [ ] 에러 처리가 되어있는가?
|
||||
- [ ] 보안 취약점이 없는가?
|
||||
- [ ] 성능 이슈가 없는가?
|
||||
- [ ] 문서가 업데이트되었는가?
|
||||
|
||||
### 리뷰어 확인사항
|
||||
- [ ] 코드 로직이 명확한가?
|
||||
- [ ] 중복 코드가 없는가?
|
||||
- [ ] 변수명이 의미있는가?
|
||||
- [ ] 함수가 단일 책임을 가지는가?
|
||||
- [ ] 테스트가 충분한가?
|
||||
|
||||
---
|
||||
|
||||
## 도구 및 린터 설정
|
||||
|
||||
### ESLint
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": ["eslint:recommended"],
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 2],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"no-var": "error",
|
||||
"prefer-const": "error",
|
||||
"no-console": "warn"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Prettier
|
||||
|
||||
```json
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 80
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 참고 자료
|
||||
|
||||
- [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript)
|
||||
- [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html)
|
||||
- [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices)
|
||||
- [Clean Code JavaScript](https://github.com/ryanmcdermott/clean-code-javascript)
|
||||
|
||||
---
|
||||
|
||||
*마지막 업데이트: 2025-12-11*
|
||||
Reference in New Issue
Block a user