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
15 changes: 13 additions & 2 deletions .github/workflows/cd-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ jobs:
--protocol tcp --port 22 \
--cidr ${{ steps.ip.outputs.ipv4 }}/32

- name: Copy docker-compose to EC2
- name: Copy infra files to EC2
uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_KEY }}
source: infra/docker-compose.prod.yml
source: infra/docker-compose.prod.yml,infra/sql/schema.sql
target: ~/maggom
strip_components: 1

Expand All @@ -104,6 +104,17 @@ jobs:

cd ~/maggom
docker compose -f docker-compose.prod.yml --env-file .env down
docker compose -f docker-compose.prod.yml --env-file .env up -d postgres
sleep 5

# 테이블 없으면 schema.sql 실행 (최초 배포 시 1회)
source .env
TABLE_EXISTS=$(docker exec maggom-postgres psql -U $DB_USERNAME -d $DB_NAME -tAc "SELECT 1 FROM information_schema.tables WHERE table_name='marathon_event'" 2>/dev/null)
if [ "$TABLE_EXISTS" != "1" ]; then
docker cp sql/schema.sql maggom-postgres:/schema.sql
docker exec maggom-postgres psql -U $DB_USERNAME -d $DB_NAME -f /schema.sql
fi

docker compose -f docker-compose.prod.yml --env-file .env up -d

- name: Remove IP from Security Group
Expand Down
120 changes: 103 additions & 17 deletions infra/sql/schema.sql
Original file line number Diff line number Diff line change
@@ -1,38 +1,124 @@
-- Ma-GGom Backend Schema
-- 최초 배포 시 한 번만 실행

-- ── 1. 회원/구독 ─────────────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS member (
id BIGSERIAL PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
role VARCHAR(20) NOT NULL DEFAULT 'MEMBER',
is_verified BOOLEAN NOT NULL DEFAULT true,
is_verified BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS subscription_preference (
id BIGSERIAL PRIMARY KEY,
member_id BIGINT NOT NULL REFERENCES member (id) ON DELETE CASCADE,
receive_days VARCHAR(50) NOT NULL DEFAULT 'MON,WED,FRI',
receive_time TIME NOT NULL DEFAULT '08:00:00',
pref_regions TEXT NOT NULL DEFAULT '["수도권"]',
pref_distances TEXT NOT NULL DEFAULT '["10K","HALF"]',
include_small BOOLEAN NOT NULL DEFAULT true
id BIGSERIAL PRIMARY KEY,
member_id BIGINT NOT NULL UNIQUE REFERENCES member (id) ON DELETE CASCADE,
receive_days VARCHAR(50) NOT NULL DEFAULT 'MON,WED,FRI',
receive_time TIME NOT NULL DEFAULT '08:00:00',
pref_regions TEXT NOT NULL DEFAULT '["수도권"]',
pref_distances TEXT NOT NULL DEFAULT '["10K","HALF"]',
include_small BOOLEAN NOT NULL DEFAULT TRUE
);

-- ── 2. 마라톤 이벤트 (API + 크롤러 공용) ──────────────────────────────────────

CREATE TABLE IF NOT EXISTS marathon_event (
id BIGSERIAL PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
event_date DATE NOT NULL,
region VARCHAR(100) NOT NULL,
distances TEXT NOT NULL,
distances TEXT NOT NULL DEFAULT '[]',
reg_start_date TIMESTAMP NOT NULL,
reg_end_date TIMESTAMP,
link_url VARCHAR(500) NOT NULL,
is_major BOOLEAN NOT NULL DEFAULT FALSE,
event_scale VARCHAR(20) NOT NULL DEFAULT 'UNKNOWN',
link_url TEXT NOT NULL,
status VARCHAR(20) NOT NULL,
source_name VARCHAR(100) NOT NULL,
source_url VARCHAR(500) NOT NULL,
crawled_at_kst TIMESTAMP NOT NULL
source_name VARCHAR(100),
source_url TEXT,
crawled_at_kst TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX IF NOT EXISTS uq_marathon_event_dedup
ON marathon_event (title, event_date, region, link_url);
CREATE INDEX IF NOT EXISTS idx_marathon_event_event_date
ON marathon_event (event_date);
CREATE INDEX IF NOT EXISTS idx_marathon_event_status
ON marathon_event (status);
CREATE INDEX IF NOT EXISTS idx_marathon_event_event_scale
ON marathon_event (event_scale);

-- ── 3. 크롤러 raw 데이터 ──────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS raw_crawled_data (
id BIGSERIAL PRIMARY KEY,
source VARCHAR(100) NOT NULL,
payload JSONB NOT NULL,
parsed_status VARCHAR(20) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_raw_crawled_data_source_created_at
ON raw_crawled_data (source, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_raw_crawled_data_parsed_status
ON raw_crawled_data (parsed_status);

-- ── 4. 연간 재개최 추적 ───────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS marathon_event_watch (
id BIGSERIAL PRIMARY KEY,
watch_key VARCHAR(255) NOT NULL UNIQUE,
base_title VARCHAR(255) NOT NULL,
sample_title VARCHAR(255) NOT NULL,
last_event_date DATE NOT NULL,
expected_event_date DATE NOT NULL,
detected_event_date DATE,
status VARCHAR(20) NOT NULL,
source_name VARCHAR(100),
source_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_marathon_event_watch_status
ON marathon_event_watch (status);
CREATE INDEX IF NOT EXISTS idx_marathon_event_watch_expected_event_date
ON marathon_event_watch (expected_event_date);

CREATE TABLE IF NOT EXISTS marathon_event_watch_seed (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
event_date DATE NOT NULL,
source_name VARCHAR(100),
source_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX IF NOT EXISTS uq_marathon_event_watch_seed_dedup
ON marathon_event_watch_seed (title, event_date, COALESCE(source_name, ''), COALESCE(source_url, ''));

-- ── 5. 크롤러 소스 레지스트리 ─────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS crawler_source_registry (
id BIGSERIAL PRIMARY KEY,
source_name VARCHAR(100) NOT NULL UNIQUE,
source_url TEXT,
source_kind VARCHAR(30) NOT NULL DEFAULT 'UNKNOWN',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_crawled_at TIMESTAMPTZ,
last_success_at TIMESTAMPTZ,
last_status VARCHAR(20) NOT NULL DEFAULT 'UNKNOWN',
last_event_count INTEGER NOT NULL DEFAULT 0,
last_rare_event_count INTEGER NOT NULL DEFAULT 0,
success_count INTEGER NOT NULL DEFAULT 0,
fail_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT
);

CREATE INDEX IF NOT EXISTS idx_marathon_event_status_region
ON marathon_event (status, region);
CREATE INDEX IF NOT EXISTS idx_crawler_source_registry_status_enabled
ON crawler_source_registry (last_status, enabled);
Loading