From d7b1f29b669c8251475f348d550a19cc0c61eae0 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Mon, 16 Mar 2026 04:41:01 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20schema.sql=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=B6=94=EA=B0=80=20=EB=93=B1=20?= =?UTF-8?q?-=20=ED=81=AC=EB=A1=A4=EB=9F=AC=20=EC=A0=84=EC=9A=A9=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=204=EA=B0=9C=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(raw=5Fcrawled=5Fdata,=20marathon=5Fevent=5Fwatch,=20maratho?= =?UTF-8?q?n=5Fevent=5Fwatch=5Fseed,=20crawler=5Fsource=5Fregistry)=20-=20?= =?UTF-8?q?marathon=5Fevent=20=EC=BB=AC=EB=9F=BC=20=EB=B3=B4=EC=99=84=20(i?= =?UTF-8?q?s=5Fmajor,=20event=5Fscale,=20created=5Fat,=20updated=5Fat)=20-?= =?UTF-8?q?=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EB=B0=8F=20=EC=9C=A0=EB=8B=88?= =?UTF-8?q?=ED=81=AC=20=EC=A0=9C=EC=95=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-prod.yml | 15 ++++- infra/sql/schema.sql | 120 +++++++++++++++++++++++++++++----- 2 files changed, 116 insertions(+), 19 deletions(-) diff --git a/.github/workflows/cd-prod.yml b/.github/workflows/cd-prod.yml index 34b3e02..96ea1c4 100644 --- a/.github/workflows/cd-prod.yml +++ b/.github/workflows/cd-prod.yml @@ -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 @@ -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 diff --git a/infra/sql/schema.sql b/infra/sql/schema.sql index c6f0a4f..af77522 100644 --- a/infra/sql/schema.sql +++ b/infra/sql/schema.sql @@ -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);