728x90

정산내역을 작업하다 보면 추가 금액을 입력하는 경우가 종종 생긴다. 전산으로 모두 계산하지만 추가 비용을 지불해야 한다던지 추가 비용을 차감해야 한다던지 하는 예외적인 상황은 별도로 입력해주는 폼을 추가 해주고 db 에 저장해 주는것이 좋다. 

<form action="process.php" method="post">
    <td>
        <select name="operation" id="operation" style="width:60px;">
            <option value="+">+</option>
            <option value="-">-</option>
        </select>
        <input type="number" name="extra_amount" class="w180" required title="금액">원
        <button type="submit">전송</button>
    </td>
</form>

 

select box 에서 기호를 선택하고 입력하는 방식으로 구현 한다면 태그를 작성하고 post 로 받는 화면은 아래와 같이 floatval 로 변환해 주어야 마이너스로 입력이 된다. db 에 필드도 float 로 잡아 주어야 한다. 

if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $operation = $_POST['operation'];
    $amount = $_POST['extra_amount'];

    // 금액에 + 또는 - 기호 붙이기
    $extra_amount = ($operation === '-') ? -floatval($amount) : floatval($amount); // 입력값을 float으로 변환

    // 선생님 정산 테이블 업데이트
    $paramArray = [];   
    $paramArray = compact('extra_amount'); 
    $paramArray["payment"] = $total_amount - $tax + $extra_amount; 

    // 데이터베이스 업데이트
    $rsArry = $this->UpdateData("reservation_settle_teacher", $paramArray, $where_com);
}
728x90
728x90

php 엑셀다운로드 예시 
 
php 코드에서 엑셀다운로드를 사용 하려면 vendor 에서 설치하고 사용해야 한다. 
composer require phpoffice/phpspreadsheet
컴포저에서 설치 후 이용해야 한다. 

<?php
require 'vendor/autoload.php';

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;

function downloadExcel($filename = 'example.xlsx') {
    // 새로운 스프레드시트 객체 생성
    $spreadsheet = new Spreadsheet();

    // 시트 1 생성
    $sheet1 = $spreadsheet->getActiveSheet();
    $sheet1->setTitle('첫 번째 시트');
    
    // 헤더 추가
    $sheet1->setCellValue('A1', '헤더 1');
    $sheet1->setCellValue('B1', '헤더 2');
    $sheet1->setCellValue('A2', '데이터 1');
    $sheet1->setCellValue('B2', '데이터 2');

    // 시트 2 생성
    $sheet2 = $spreadsheet->createSheet();
    $sheet2->setTitle('두 번째 시트');
    
    // 헤더 추가
    $sheet2->setCellValue('A1', '헤더 A');
    $sheet2->setCellValue('B1', '헤더 B');
    $sheet2->setCellValue('A2', '데이터 A');
    $sheet2->setCellValue('B2', '데이터 B');

    // 파일 다운로드를 위한 헤더 설정
    header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
    header("Content-Disposition: attachment; filename=\"$filename\"");
    header('Cache-Control: max-age=0');

    // 엑셀 파일 작성
    $writer = new Xlsx($spreadsheet);
    $writer->save('php://output');
    exit;
}

// 함수 호출
downloadExcel('내_엑셀파일.xlsx');
?>

 
엑셀업로드예시

<?php
$host = 'localhost'; // 데이터베이스 호스트
$db = 'your_database'; // 데이터베이스 이름
$user = 'your_username'; // 사용자 이름
$pass = 'your_password'; // 비밀번호
$charset = 'utf8mb4';

$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];

try {
    $pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
    throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
?>

 
파일 업로드 후 db 에 insert 하는 예시

<?php
require 'vendor/autoload.php';

use PhpOffice\PhpSpreadsheet\IOFactory;

// 파일 업로드 처리
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['excel_file'])) {
    $file = $_FILES['excel_file']['tmp_name'];

    // 엑셀 파일 읽기
    $spreadsheet = IOFactory::load($file);

    // 모든 시트 순회
    foreach ($spreadsheet->getAllSheets() as $sheet) {
        $sheetData = $sheet->toArray();

        // 첫 번째 행은 헤더로 가정하고 데이터 삽입
        for ($row = 1; $row < count($sheetData); $row++) {
            $data = $sheetData[$row];

            // 데이터 삽입 쿼리 (여기서 예시로 'your_table'을 사용)
            $stmt = $pdo->prepare("INSERT INTO your_table (column1, column2) VALUES (?, ?)");
            $stmt->execute([$data[0], $data[1]]);
        }
    }

    echo "데이터가 성공적으로 삽입되었습니다.";
}
?>

<!-- HTML 업로드 폼 -->
<form method="post" enctype="multipart/form-data">
    <input type="file" name="excel_file" accept=".xlsx, .xls" required>
    <button type="submit">업로드</button>
</form>
728x90
728x90

쿠폰이나 구독권이나 사용 가능 기간을 start_date 와 expir_date 로 테이블에 필드를 잡았다면 현재 일자를 입력 받아서 유효한지 아닌지를 체크 하려면 count 를 사용해도 되지만 true, false 로 반환하는 방법도 있다. 아래 쿼리문은 true, false 반환하는 예시다.  

초보때 쿼리문을 뽑을때 데이터가 없으면 해당 날짜에 포함되는 데이터가 없으면 조회 되는 row 자체가 없기에 date_condition 으로 접근 자체가 안되는걸 모르고 한참 삽질을 한적이 있는데 예시와 같이 case 문으로  뽑을 수도 있다. 보통은 count 로 해서 0보다 크면 조건으로 많이 작업한다. 

SELECT 
	CASE WHEN EXISTS ( 
		SELECT 1 FROM cupon 
		WHERE user_seq = ${user_seq} AND start_date <= #{today} AND expir_date >= #{today} 
    ) 
	THEN 'true' ELSE 'false' END AS date_condition

 

count 가 조금 더 코드가 간소하고 명확하기도 하다. 

SELECT COUNT(date_condition) as date_condition_cnt
FROM cupon
WHERE user_seq=${user_seq} AND start_date <=#{today} AND expir_date >= #{today}

 

오늘은 이미 되어 있는 쿼리문이어서 다른 곳에서도 사용하는 곳이 있어서 부득이하게 수정하긴 했는데 지금 같은 케이스틑 count 가져오는게 편리한거 같다. 

728x90
728x90

체크박스 전체선택과 일부 선택하면 해제 되는 스크립트는 의외로 꽤 많은 곳에서 사용 되어 진다. 회원가입 약관동의가 대표적이고 리스트중 하나 하나 선택하는 체크박스에도 사용 된다.

  const [checkboxes, setCheckboxes] = useState({
    option1: false,
    option2: false,
    option3: false,
  });

  // 전체 체크박스가 변경되었을 때 나머지 체크박스들을 모두 체크하거나 해제
  const handleAllCheckboxChange = (event) => {
    const isChecked = event.target.checked;
    setAllChecked(isChecked);
    setCheckboxes({
      option1: isChecked,
      option2: isChecked,
      option3: isChecked,
    });
  };
  
  // 개별 체크박스가 변경되었을 때 상태 업데이트 및 전체 체크 상태 업데이트
  const handleCheckboxChange = (event) => {
    const { name, checked } = event.target;
    setCheckboxes((prevCheckboxes) => {
      const updatedCheckboxes = {
        ...prevCheckboxes,
        [name]: checked,
      };
      // 개별 체크박스가 모두 체크된 경우 전체 체크박스를 true로 설정
      const allCheckedStatus = Object.values(updatedCheckboxes).every(Boolean);
      setAllChecked(allCheckedStatus);
      return updatedCheckboxes;
    });
  };

 

선언 부분과 실제 태그를 아래에 메모해 본다. 

<article className="all">
    <input
      type="checkbox"
      className="com_chk"
      id="all"
      checked={allChecked}
      onChange={handleAllCheckboxChange}
    />
    <label htmlFor="all">전체동의 </label>
  </article>
  <ul className="list">
    <li className="item">
      <div>
        <input
          type="checkbox"
          className="com_chk"
          id="agree01"
          name="option1"
          checked={checkboxes.option1}
          onChange={handleCheckboxChange}
        />
        <label htmlFor="agree01">
          전자금융거래 기본약관{' '}
          <span className="txt_required">(필수)</span>
        </label>
      </div>
      <button className="com_btn txt">자세히</button>
    </li>
   </ul>

 

 

728x90
728x90

php 작업중 여러 테이블에 데이터를 insert 할때는 트랜지션을 걸고 모든 데이터가 완료 되었을때 commit 을하고 하나라도 오류가 있을 경우에는 rollback 작업을걸어 준다. 이때 insert 하고 난 데이터는 마지막에 commit 되기 때문에 excute 에서는 데이터 조회가 되지 못하는데 설정값을 트랜지션 시작 지점 위에 선언해 주면 excute 까지만 되어도 해당 데이터는 insert 된것으로 인지하고 select 문을 실행한다. 

 

$pdo->exec("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
try {
    // 데이터베이스 연결 설정
    $pdo = new PDO('mysql:host=localhost;dbname=your_database', 'username', 'password');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // 트랜잭션 격리 수준 설정
    $pdo->exec("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");

    // 트랜잭션 시작
    $pdo->beginTransaction();

    // 삽입할 데이터 배열
    $dataToInsert = [
        ['value1' => '데이터1', 'value2' => '데이터2'],
        ['value1' => '데이터3', 'value2' => '데이터4'],
        // 추가 데이터...
    ];

    // 데이터 삽입 및 체크
    foreach ($dataToInsert as $data) {
        // 데이터 삽입
        $stmt = $pdo->prepare("INSERT INTO your_table (column1, column2) VALUES (:value1, :value2)");
        $stmt->execute([':value1' => $data['value1'], ':value2' => $data['value2']]);

        // 삽입된 데이터 체크
        $lastId = $pdo->lastInsertId();
        $checkStmt = $pdo->prepare("SELECT * FROM your_table WHERE id = :id");
        $checkStmt->execute([':id' => $lastId]);
        $result = $checkStmt->fetch();

        if (!$result) {
            // 데이터가 존재하지 않으면 롤백하고 종료
            $pdo->rollBack();
            echo "데이터 삽입에 실패했습니다: " . $data['value1'];
            exit;
        }
    }

    // 모든 데이터가 정상적으로 삽입되면 커밋
    $pdo->commit();
    echo "모든 데이터가 성공적으로 삽입되었습니다.";

} catch (Exception $e) {
    // 예외 처리
    $pdo->rollBack();
    echo "오류 발생: " . $e->getMessage();
}
 

 

 

트랜지션에는 4가지 종류가 있는데 각 상황에 맞게 시작지점에 선언해 주면 된다.

 

 

READ UNCOMMITTED: 다른 트랜잭션이 커밋하지 않은 변경 사항을 읽을 수 있습니다. (Dirty Reads 가능)
READ COMMITTED: 다른 트랜잭션이 커밋한 변경 사항만 읽을 수 있습니다. (Dirty Reads 방지)
REPEATABLE READ: 트랜잭션 시작 시점의 데이터를 읽습니다. (Phantom Reads 방지)
SERIALIZABLE: 가장 높은 격리 수준으로, 트랜잭션이 직렬화된 것처럼 동작합니다. (Phantom Reads 및 모든 충돌 방지)

 

한참 찾았는데 실제로 내가 해봤을때 되는 코드는 READ UNCOMMITTED 설정이니 참고 하면 좋겠다.

728x90
728x90

scss sass 등 css 스타일을 간단하게 사용 할수 있는 코드를 사용 하려면 react 나 vue 에서는 설치만 하면 바로 사용이 가능하지만 php 에서는 css 파일로 컴파일을 해줘야 한다.

vscode 에서 작업 중 이라면 Live sass Compiler 플러그인을 설치하면 저장할때 자동 컴파일 설정을 해두면 자동으로 컴파일이 되고 css 파일도 생성이 된다. 

 

 

이때 별도로 설정을 하지 않는다면 scss 파일이 있는 폴더 내에 동일한 이름으로 생성이 되는데 css 폴더만 별도로 분리해서 관리 하고 싶다면 설정을 아래와 같이 주면 된다. 

 

savePath 에 경로를 설정해주고 한참 찾았는데 "liveSassCompile.settings.autoprefix"  이 부분도 빈 배열로 넣어 주어야 아이폰등에서 적용 되는 코드가 같이 생성이 된다. 이 값이 없을 경우는 아이폰 파이어폭스 등에서만 적용이 되는 코드는 생성이 되지 않는다. 요즘 버전 업이 많이 되어서 그거 불필요한 코드 이긴 하지만 다양하 브라우저에서 대응하게 하고 싶다면 위 설정도 넣어 주는것이 좋다. 

 

    "git.enableSmartCommit": true,
    "editor.formatOnSave": true,
    "liveSassCompile.settings.formats": [
        {
            "format": "expanded",
            "extensionName": ".css",
            "savePath": "~/../css",
            //"savePath": null, 
            "savePathReplacementPairs": null
        }
    ],
    "liveSassCompile.settings.generateMap": false,
    "liveSassCompile.settings.useNewCompiler": false,
    "liveSassCompile.settings.autoprefix": [
    
    ],

 

php 에서도 컴포즈 같은걸로 다운 받으면 바로 자동 컴파일이 되면 좋겠지만 아쉽지만 요렇게 저장만 누르면 자동으로 컴파일을 해주니 상당히 편리한거 같다. php 는 곧 없어질거 같았지만 아직 의외로 많은 곳에서 쓰고 있고 버전업도 계속 되고 있어서 리액트나 뷰 처럼 변수가 동적으로 움직이는 시스템이 된다면 상당히 경쟁력이 있을거 같은데 그 점을 제외하면 서버비용도 저렴하고 코드도 쉽고 페이지도 빠르게 떠서 좋은거 같다. 

728x90
728x90

예시는 vue3 에서 vuetify 를 사용중일때 년/월/일 을 차례로 입력할때 enter key 로 자료를 빠르게 등록 하고자 할때 사용하면 유용한 스크립트 이다.  

<template>
  <VTextField
    ref="input1"
    maxlength="4"
    variant="underlined"
    class="h-small inputNext"
    @keyup.enter="focusNextInput($event)"
  />
  <VTextField
    ref="input2"
    maxlength="4"
    variant="underlined"
    class="h-small inputNext"
    @keyup.enter="focusNextInput($event)"
  />
  <VTextField
    ref="input3"
    maxlength="4"
    variant="underlined"
    class="h-small inputNext"
    @keyup.enter="focusNextInput($event)"
  />
</template>

<script setup>
import { ref } from 'vue';

function focusNextInput(event) {
  // 현재 입력 필드의 다음 형제 요소를 찾음
  const currentInput = event.target;
  const nextInput = currentInput.nextElementSibling;

  if (nextInput && nextInput.classList.contains('inputNext')) {
    nextInput.focus(); // 다음 입력 필드로 포커스 이동
  }
}
</script>

 

여러 필드가 있어도 dom 에서 그려지는 순서대로 이동하기 때문에 인덱스값 없이 함수 하나만 호출 하면 된다. 

 

아래 예제는 maxlength 값에 따라 글자수 만큼 채워지면 다음 칸으로 이동하는 스크립트다. 

<template>
  <VTextField
    ref="input1"
    :maxlength="4"
    variant="underlined"
    class="h-small inputNext"
    @keyup.enter="focusNextInput($event)"
    @input="checkInput($event)"
  />
  <VTextField
    ref="input2"
    :maxlength="2"
    variant="underlined"
    class="h-small inputNext"
    @keyup.enter="focusNextInput($event)"
    @input="checkInput($event)"
  />
  <VTextField
    ref="input3"
    :maxlength="4"
    variant="underlined"
    class="h-small inputNext"
    @keyup.enter="focusNextInput($event)"
    @input="checkInput($event)"
  />
  <VTextField
    ref="input4"
    :maxlength="3"
    variant="underlined"
    class="h-small inputNext"
    @keyup.enter="focusNextInput($event)"
    @input="checkInput($event)"
  />
</template>

<script setup>
import { ref } from 'vue';

function focusNextInput(event) {
  const currentInput = event.target;
  const nextInput = currentInput.nextElementSibling;

  if (nextInput && nextInput.classList.contains('inputNext')) {
    nextInput.focus();
  }
}

function checkInput(event) {
  const currentInput = event.target;
  const maxLength = currentInput.getAttribute('maxlength'); // maxlength 값 가져오기

  // 입력된 글자의 길이를 확인
  if (currentInput.value.length >= maxLength) {
    focusNextInput(event); // 다음 입력 필드로 포커스 이동
  }
}
</script>
728x90
728x90

회원가입 시 회원 비밀번호는 복호화로 암호가 풀어져서는 안된다. 그래서 단방향 암호화를 사용하는데 예전에는 md5 를 사용 했지만 요즘은 php 내장 함수 password_hash 가 유용한거 같다.

<?php
$password = '사용자_입력_비밀번호';
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
echo $hashedPassword;
?>

 

저장 할때는 password_hash 로 하고 암호를 비교 할때는 password_verify 로 사용한다. 

<?php
$inputPassword = '사용자_입력_비밀번호';
$storedHashedPassword = '데이터베이스에서_가져온_해시된_비밀번호';

if (password_verify($inputPassword, $storedHashedPassword)) {
    echo '비밀번호가 일치합니다.';
} else {
    echo '비밀번호가 일치하지 않습니다.';
}
?>
728x90
728x90

이 예제에서는 버튼 요소에 data-action과 data-target 속성을 추가하여 버튼의 동작과 대상 폼을 지정했습니다. JavaScript에서는 이 data- 속성을 읽어 적절한 동작을 수행하도록 구현했습니다.

data- 속성은 이처럼 HTML과 JavaScript 간의 데이터 전달에 유용하게 사용될 수 있습니다.

<div id="myElement" data-color="blue" data-size="large">...</div>

const myElement = document.getElementById('myElement');
console.log(myElement.dataset.color); // "blue"
console.log(myElement.dataset.size); // "large"
<button id="myButton" data-action="submit" data-target="#myForm">Submit</button>
<form id="myForm">
  <!-- form fields -->
</form>
<script>
const myButton = document.getElementById('myButton');
myButton.addEventListener('click', () => {
  const action = myButton.dataset.action;
  const target = myButton.dataset.target;

  if (action === 'submit') {
    const form = document.querySelector(target);
    form.submit();
  }
});

</script>
728x90
728x90

첨부파일을 드래그로 순서 변경 가능하게 구현한 스크립트 

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Drag and Drop</title>
    <style>
        .file-list-box {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            justify-content: flex-start;
            align-items: center;
            border: 2px dashed #ccc;
            padding: 10px;
            min-height: 100px;
        }

        .file-list-box .img-box {
            width: 100px;
            height: 100px;
            border-radius: 8px;
            overflow: hidden;
        }

        .file-list-box .img-box img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }

        .over {
            border: 2px dashed #000;
        }
    </style>
</head>
<body>
    <div class="file-box" data-max-files="5" id="file-box-1">
        <div class="file-s-tit">사진올리기</div>
        <div class="file-list-box attachBookingList" data-id="1"></div>
        <button class="btn-file" type="button" onclick="openFileInput(1)">사진추가</button>
        <input type="file" name="addFile" id="fileInput-1" class="file-input" style="display: none;" accept="image/*" multiple>
    </div>

    <div class="file-box" data-max-files="5" id="file-box-2">
        <div class="file-s-tit">사진올리기</div>
        <div class="file-list-box attachBookingList" data-id="2"></div>
        <button class="btn-file" type="button" onclick="openFileInput(2)">사진추가</button>
        <input type="file" name="addFile" id="fileInput-2" class="file-input" style="display: none;" accept="image/*" multiple>
    </div>

    <script>
        function openFileInput(id) {
            document.getElementById(`fileInput-${id}`).click();
        }

        function handleFiles(files, targetBox) {
            const maxFiles = parseInt(targetBox.closest('.file-box').dataset.maxFiles);
            const currentFiles = targetBox.querySelectorAll('.img-box').length;
            const totalFiles = currentFiles + files.length;

            if (totalFiles > maxFiles) {
                alert(`최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`);
                return;
            }

            Array.from(files).forEach(file => {
                if (!file.type.startsWith('image/')) return;

                const reader = new FileReader();
                reader.onload = function(e) {
                    const newImage = document.createElement('div');
                    newImage.classList.add('img-box');
                    newImage.innerHTML = `<img src="${e.target.result}" alt="이미지">`;
                    targetBox.appendChild(newImage);
                };
                reader.readAsDataURL(file);
            });
        }

        document.addEventListener('DOMContentLoaded', () => {
            document.querySelectorAll('.file-input').forEach(input => {
                input.addEventListener('change', function(event) {
                    const targetBox = document.querySelector(`.file-list-box[data-id="${this.id.split('-')[1]}"]`);
                    handleFiles(event.target.files, targetBox);
                    this.value = '';
                });
            });

            document.querySelectorAll('.file-list-box').forEach(box => {
                box.addEventListener('dragover', function(event) {
                    event.preventDefault();
                    event.stopPropagation();
                    this.classList.add('over');
                });

                box.addEventListener('dragleave', function(event) {
                    event.preventDefault();
                    event.stopPropagation();
                    this.classList.remove('over');
                });

                box.addEventListener('drop', function(event) {
                    event.preventDefault();
                    event.stopPropagation();
                    this.classList.remove('over');
                    const files = event.dataTransfer.files;
                    handleFiles(files, this);
                });
            });
        });
    </script>
</body>
</html>

 

// 파일 업로드 처리 함수
function handleFileUpload(event, listBoxSelector) {
  const files = event.target.files;
  const fileListBox = document.querySelector(listBoxSelector);

  // 파일 목록 표시
  for (let i = 0; i < files.length; i++) {
    const fileItem = document.createElement('div');
    fileItem.classList.add('file-item');
    fileItem.textContent = files[i].name;
    fileItem.draggable = true;
    fileItem.addEventListener('dragstart', handleDragStart);
    fileItem.addEventListener('dragover', handleDragOver);
    fileItem.addEventListener('drop', handleDrop);
    fileListBox.appendChild(fileItem);
  }
}

// 드래그 시작 이벤트 핸들러
function handleDragStart(event) {
  event.dataTransfer.setData('text/plain', null);
  event.currentTarget.classList.add('dragging');
}

// 드래그 오버 이벤트 핸들러
function handleDragOver(event) {
  event.preventDefault();
  event.currentTarget.classList.add('drag-over');
}

// 드롭 이벤트 핸들러
function handleDrop(event) {
  event.preventDefault();
  event.currentTarget.classList.remove('drag-over');

  const draggedItem = document.querySelector('.dragging');
  const dropTarget = event.currentTarget;
  const fileListBox = event.currentTarget.parentNode;

  // 파일 목록 순서 변경
  fileListBox.insertBefore(draggedItem, dropTarget);
}

// 파일 추가 버튼 클릭 이벤트 핸들러
document.querySelectorAll('.btn-file').forEach(button => {
  button.addEventListener('click', () => {
    const fileInput = button.parentNode.querySelector('.file-input');
    fileInput.click();
  });
});

// 파일 선택 이벤트 핸들러
document.querySelectorAll('.file-input').forEach(input => {
  input.addEventListener('change', (event) => {
    const listBoxSelector = event.target.parentNode.parentNode.querySelector('.file-list-box').classList[1];
    handleFileUpload(event, `.${listBoxSelector}`);
  });
});
728x90

+ Recent posts