Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# asac_de2_infra_1st
ASAC 데이터 엔지니어 2기 인프라 프로젝트 1조
50 changes: 50 additions & 0 deletions app/draw_winner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pymysql
import os
import random
from datetime import datetime

# DB 설정 (기존 환경변수 활용)
DB_CONFIG = {
"host": os.environ.get('DB_WRITER_HOST'),
"user": os.environ.get('DB_USER', 'admin'),
"password": os.environ.get('DB_PASSWORD'),
"database": os.environ.get('DB_NAME', 'raffle_db'),
"cursorclass": pymysql.cursors.DictCursor
}

def run_draw():
conn = pymysql.connect(**DB_CONFIG)
try:
with conn.cursor() as cursor:
# 1. 추첨 대상 찾기 (종료시간 지남 + 아직 추첨 안 됨)
cursor.execute("""
SELECT id, title FROM raffle_items
WHERE end_time <= NOW() AND is_drawn = FALSE
""")
items_to_draw = cursor.fetchall()

for item in items_to_draw:
# 2. 해당 상품 응모자 중 랜덤 1명 추출
cursor.execute("SELECT user_id FROM raffle_entries WHERE item_id = %s", (item['id'],))
entries = cursor.fetchall()

if entries:
winner = random.choice(entries)
# 3. 당첨자 기록 및 추첨 완료 처리
cursor.execute("""
UPDATE raffle_items
SET winner_id = %s, is_drawn = TRUE
WHERE id = %s
""", (winner['user_id'], item['id']))
print(f"[{datetime.now()}] {item['title']} 추첨 완료! 당첨자 ID: {winner['user_id']}")
else:
# 응모자가 없는 경우에도 종료 처리는 해야 함
cursor.execute("UPDATE raffle_items SET is_drawn = TRUE WHERE id = %s", (item['id'],))
print(f"[{datetime.now()}] {item['title']} 응모자 없음 처리.")

conn.commit()
finally:
conn.close()

if __name__ == "__main__":
run_draw()
91 changes: 91 additions & 0 deletions app/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASACDE RAFFLE</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');
body { font-family: 'Inter', sans-serif; background-color: #ffffff; }
.hero-bg { background-color: #242c38; }
</style>
</head>
<body>
<nav class="flex justify-between items-center px-8 py-4 border-b border-gray-100 sticky top-0 bg-white z-50">
<div class="text-2xl font-black tracking-tighter italic">ASACDE 1ST RAFFLE</div>
<div class="space-x-6 text-sm font-medium text-gray-600">
<a href="/" class="hover:text-black">HOME</a>
{% if is_logged_in %}
<a href="/mypage" class="hover:text-black">MYPAGE</a>
<a href="/api/logout" class="text-red-500">LOGOUT</a>
{% else %}
<a href="/login" class="hover:text-black">LOGIN</a>
<a href="/signup" class="hover:text-black">SIGNUP</a>
{% endif %}
</div>
</nav>

<div class="hero-bg w-full aspect-[21/9] flex items-center justify-between px-16 relative overflow-hidden">
<div class="text-white z-10">
<h1 class="text-6xl font-black leading-none mb-2">WEEKLY<br>DROP LIST</h1>
<p class="text-lg opacity-80">04.01 - 04.30 LIMITED EDITION</p>
</div>
<div class="w-1/3 z-10">
<img src="https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?w=800&q=80" class="drop-shadow-2xl rotate-12 hover:rotate-0 transition-transform duration-500">
</div>
<div class="absolute right-0 top-0 w-1/2 h-full bg-gradient-to-l from-blue-900/20 to-transparent"></div>
</div>

<div class="max-w-7xl mx-auto px-8 py-12">
<h2 class="text-2xl font-bold mb-8">🔥 지금 가장 핫한 래플</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{% for item in items %}
<div class="group cursor-pointer">
<div class="relative bg-gray-100 rounded-xl overflow-hidden aspect-square mb-4">
<img src="{{ item.image_url }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
<div class="absolute top-4 left-4 bg-black text-white text-[10px] px-2 py-1 font-bold">DRAW</div>
</div>
<h3 class="font-bold text-lg mb-1 truncate">{{ item.title }}</h3>
<p class="text-gray-500 text-sm mb-3 line-clamp-1">{{ item.description }}</p>
<div class="timer text-red-500 font-bold text-sm mb-4" data-endtime="{{ item.end_time }}">--:--:--:--</div>
<button onclick="applyRaffle({{ item.id }})" class="w-full py-3 bg-white border border-gray-200 rounded-lg font-bold text-sm hover:bg-black hover:text-white transition-colors">
응모하기
</button>
</div>
{% endfor %}
</div>
</div>

<script>
// 타이머 및 응모 로직은 기존과 동일 (CSS 클래스만 깔끔하게 유지)
setInterval(() => {
document.querySelectorAll('.timer').forEach(el => {
const end = new Date(el.getAttribute('data-endtime')).getTime();
const now = new Date().getTime();
const distance = end - now;
if (distance < 0) { el.innerHTML = "CLOSED"; return; }
const d = Math.floor(distance / (86400000));
const h = Math.floor((distance % 86400000) / 3600000);
const m = Math.floor((distance % 3600000) / 60000);
const s = Math.floor((distance % 60000) / 1000);
el.innerHTML = `${d}D ${h}H ${m}M ${s}S LEFT`;
});
}, 1000);

async function applyRaffle(itemId) {
const res = await fetch('/api/apply', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({item_id: itemId})
});
if (res.status === 401) {
if(confirm("로그인 후 응모 가능합니다. 로그인 하시겠습니까?")) window.location.href = '/login';
} else {
const data = await res.json();
alert(data.message);
}
}
</script>
</body>
</html>
77 changes: 77 additions & 0 deletions app/templates/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>KREAM Style Login</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');
body { font-family: 'Inter', sans-serif; }
</style>
</head>
<body class="bg-gray-50">
<div class="max-w-md mx-auto py-24 px-4">
<div class="text-4xl font-black italic tracking-tighter text-center mb-16">
<a href="/">ASACDE 1ST RAFFLE</a>
</div>

<div class="bg-white rounded-2xl p-10 shadow-sm border border-gray-100">
<h2 class="text-xl font-bold mb-10">로그인</h2>

<div class="space-y-6">
<div>
<label class="block text-xs font-bold mb-2">아이디</label>
<input type="text" id="uid" placeholder="아이디를 입력하세요"
class="w-full py-4 border-b-2 border-gray-100 focus:border-black outline-none text-sm transition-colors">
</div>

<div>
<label class="block text-xs font-bold mb-2">비밀번호</label>
<input type="password" id="pw" placeholder="비밀번호를 입력하세요"
class="w-full py-4 border-b-2 border-gray-100 focus:border-black outline-none text-sm transition-colors">
</div>

<div class="pt-6">
<button onclick="login()" class="w-full py-5 bg-black text-white rounded-lg font-bold hover:bg-gray-800 transition-colors">
로그인
</button>
</div>
</div>

<div class="flex justify-center space-x-6 mt-10 text-xs text-gray-400">
<a href="/signup" class="hover:text-black hover:font-bold">회원가입</a>
<span>|</span>
<a href="#" class="hover:text-black hover:font-bold">아이디 찾기</a>
<span>|</span>
<a href="#" class="hover:text-black hover:font-bold">비밀번호 찾기</a>
</div>
</div>
</div>

<script>
async function login() {
const uid = document.getElementById('uid').value;
const pw = document.getElementById('pw').value;

if(!uid || !pw) {
alert("아이디와 비밀번호를 모두 입력해주세요.");
return;
}

const res = await fetch('/api/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: uid, password: pw})
});

if(res.ok) {
// 로그인 성공 시 메인으로 이동
window.location.href = '/';
} else {
const data = await res.json();
alert(data.message);
}
}
</script>
</body>
</html>
45 changes: 45 additions & 0 deletions app/templates/mypage.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>My Page</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50">
<div class="max-w-2xl mx-auto py-16 px-4">
<div class="flex items-center justify-between mb-12">
<h1 class="text-3xl font-black italic">MY PAGE</h1>
<a href="/" class="text-sm font-bold text-gray-400 hover:text-black">HOME ⬅️</a>
</div>

<div class="bg-white rounded-2xl p-8 shadow-sm border border-gray-100">
<div class="flex items-center mb-10">
<div class="w-16 h-16 bg-gray-200 rounded-full mr-4"></div>
<div>
<h2 class="text-xl font-bold">{{ user_id }}</h2>
<p class="text-gray-400 text-sm">PRO MEMBER</p>
</div>
</div>

<h3 class="font-bold mb-6 text-gray-900 border-b pb-4">나의 응모 내역</h3>
<div class="space-y-6">
{% if not history %}
<p class="text-center py-10 text-gray-400">응모 내역이 없습니다.</p>
{% else %}
{% for item in history %}
<div class="flex justify-between items-center">
<div>
<p class="font-bold">{{ item.title }}</p>
<p class="text-xs text-gray-400">{{ item.apply_date }}</p>
</div>
<span class="text-sm font-bold {% if '대기' in item.status %}text-blue-500{% else %}text-gray-300{% endif %}">
{{ item.status }}
</span>
</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
</body>
</html>
73 changes: 73 additions & 0 deletions app/templates/signup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>KREAM Style Signup</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');
body { font-family: 'Inter', sans-serif; }
</style>
</head>
<body class="bg-gray-50">
<div class="max-w-md mx-auto py-24 px-4">
<div class="text-4xl font-black italic tracking-tighter text-center mb-16">
<a href="/">ASACDE 1ST RAFFLE</a>
</div>

<div class="bg-white rounded-2xl p-10 shadow-sm border border-gray-100">
<h2 class="text-xl font-bold mb-10">회원가입</h2>

<div class="space-y-6">
<div>
<label class="block text-xs font-bold mb-2">아이디</label>
<input type="text" id="uid" placeholder="아이디를 입력하세요"
class="w-full py-4 border-b-2 border-gray-100 focus:border-black outline-none text-sm transition-colors">
</div>

<div>
<label class="block text-xs font-bold mb-2">비밀번호</label>
<input type="password" id="pw" placeholder="비밀번호를 입력하세요"
class="w-full py-4 border-b-2 border-gray-100 focus:border-black outline-none text-sm transition-colors">
</div>

<div class="pt-6">
<button onclick="signup()" class="w-full py-5 bg-black text-white rounded-lg font-bold hover:bg-gray-800 transition-colors">
가입 완료
</button>
</div>
</div>

<div class="text-center mt-10 text-xs text-gray-400">
이미 회원이신가요? <a href="/login" class="hover:text-black hover:font-bold underline">로그인</a>
</div>
</div>
</div>

<script>
async function signup() {
const uid = document.getElementById('uid').value;
const pw = document.getElementById('pw').value;

if(!uid || !pw) {
alert("아이디와 비밀번호를 모두 입력해주세요.");
return;
}

const res = await fetch('/api/signup', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: uid, password: pw})
});

const data = await res.json();
alert(data.message);

if(res.ok) {
// 가입 성공 시 로그인 페이지로 이동
window.location.href = '/login';
}
}
</script>
</body>
</html>
20 changes: 20 additions & 0 deletions k8s/base/cronjob.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: raffle-draw-job
spec:
schedule: "* * * * *" # 매 1분마다 실행
jobTemplate:
spec:
template:
spec:
containers:
- name: draw-worker
image: 486053612615.dkr.ecr.eu-west-2.amazonaws.com/my-raffle-app:v1
command: ["python", "draw_winner.py"]
envFrom:
- configMapRef:
name: raffle-config
- secretRef:
name: raffle-secret
restartPolicy: OnFailure
22 changes: 11 additions & 11 deletions k8s/base/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ spec:
app: data-pipeline-app
spec:
containers:
- name: app
image: data-pipeline-app:latest
- name: app-pipeline-app
image: data-pipeline-app
imagePullPolicy: Always
ports:
- containerPort: 8000
readinessProbe:
httpGet:
path: /health
port: 8000
livenessProbe:
httpGet:
path: /health
port: 8000
- containerPort: 8080
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
Loading
Loading