# PostgreSQL SSH 터널 및 데이터베이스 접속 가이드

## 📋 목차
1. [SSH 터널 설정](#ssh-터널-설정)
2. [PostgreSQL 접속](#postgresql-접속)
3. [Python으로 DB 조회](#python으로-db-조회)
4. [주요 테이블 구조](#주요-테이블-구조)
5. [자주 사용하는 쿼리](#자주-사용하는-쿼리)
6. [문제 해결](#문제-해결)

---

## SSH 터널 설정

### 1. 현재 SSH 터널 확인
```bash
# 실행 중인 SSH 터널 확인
ps aux | grep "ssh.*5433" | grep -v grep

# 특정 포트 사용 확인
netstat -tlnp | grep 5433
lsof -i :5433
```

### 2. SSH 터널 생성
```bash
# 기본 SSH 터널 생성 (백그라운드 실행)
ssh -f -N -L 5433:localhost:5432 admin@124.55.18.179 -p 51123

# StrictHostKeyChecking 비활성화 (테스트용)
ssh -o StrictHostKeyChecking=no -f -N -L 5433:localhost:5432 admin@124.55.18.179 -p 51123

# 옵션 설명:
# -f : 백그라운드 실행
# -N : 원격 명령 실행하지 않음 (포트 포워딩만)
# -L : 로컬 포트 포워딩 (로컬포트:원격호스트:원격포트)
# -p : SSH 포트 지정
```

### 3. SSH 터널 종료
```bash
# 프로세스 ID 찾기
ps aux | grep "ssh.*5433"

# 프로세스 종료
sudo kill [PID]

# 또는 한 번에
sudo pkill -f "ssh.*5433"
```

---

## PostgreSQL 접속

### 1. 연결 정보
```
Host: localhost (SSH 터널 사용시) 또는 192.168.219.45 (직접 연결)
Port: 5433 (터널 로컬 포트) 또는 5432 (직접 연결)
Database: main_db (기존 auth_db는 더 이상 사용 안함)
Username: robeings
Password: robeings
```

### 2. psql 명령어로 접속
```bash
# psql 설치 (Ubuntu/Debian)
sudo apt-get install postgresql-client

# DB 접속 (51123 서버에서 직접)
psql postgresql://robeings:robeings@localhost:5432/main_db

# 또는 (51124 서버에서 SSH 터널 사용)
psql postgresql://robeings:robeings@localhost:5433/main_db

# 또는
psql -h localhost -p 5432 -U robeings -d main_db
```

### 3. psql 기본 명령어
```sql
-- 테이블 목록 보기
\dt

-- 특정 테이블 구조 보기
\d gmail_token
\d users

-- 데이터베이스 목록
\l

-- 현재 데이터베이스 정보
\conninfo

-- 종료
\q
```

---

## Python으로 DB 조회

### 1. 필요한 패키지 설치
```bash
pip install psycopg2-binary
```

### 2. 기본 연결 및 조회
```python
import psycopg2
import json

# DB 연결 (51123 서버에서)
conn = psycopg2.connect('postgresql://robeings:robeings@localhost:5432/main_db')

# DB 연결 (51124 서버에서 SSH 터널 사용)
conn = psycopg2.connect('postgresql://robeings:robeings@localhost:5433/main_db')
cur = conn.cursor()

# 쿼리 실행
cur.execute("SELECT * FROM user LIMIT 5")
rows = cur.fetchall()

for row in rows:
    print(row)

# 연결 종료
cur.close()
conn.close()
```

### 3. 안전한 쿼리 실행 (파라미터 바인딩)
```python
# SQL Injection 방지를 위해 항상 파라미터 바인딩 사용
user_id = 'b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0'
cur.execute(
    "SELECT * FROM user WHERE id = %s",
    (user_id,)  # 튜플로 전달
)
```

---

## 주요 테이블 구조

### 1. users 테이블
```sql
CREATE TABLE user (
    id UUID PRIMARY KEY,
    email VARCHAR UNIQUE,
    name VARCHAR,
    picture VARCHAR,
    oauth_provider VARCHAR,
    oauth_id VARCHAR,
    is_active BOOLEAN,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    last_login_at TIMESTAMP,
    username VARCHAR
);
```

### 2. gmail_token 테이블
```sql
CREATE TABLE gmail_token (
    id SERIAL PRIMARY KEY,
    user_id UUID REFERENCES user(id),
    slack_id VARCHAR(100),   -- Slack 사용자 ID (새로 추가)
    is_equipped BOOLEAN,
    has_token BOOLEAN,       -- token_data 존재 여부
    access_token TEXT,       -- 개별 컬럼으로 분리
    refresh_token TEXT,      -- 개별 컬럼으로 분리
    token_type VARCHAR,
    expires_at FLOAT,
    token_data JSONB,        -- 레거시 호환용
    oauth_config JSONB,      -- OAuth 설정 정보
    scopes JSONB,           -- Gmail API 권한 목록
    metadata JSONB,
    expiry TIMESTAMP,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    robeing_id VARCHAR,      -- 로빙 ID (rb8001 등)
    equipped_to VARCHAR
);
```

### 3. conversation_log 테이블
```sql
CREATE TABLE conversation_log (
    id SERIAL PRIMARY KEY,
    robeing_id VARCHAR,
    channel_id VARCHAR,
    message VARCHAR,
    response VARCHAR,
    intent VARCHAR,
    confidence DOUBLE PRECISION,
    timestamp TIMESTAMP,
    user_id UUID REFERENCES user(id),  -- NULL 허용으로 변경됨
    slack_user_id VARCHAR(100)          -- Slack ID 직접 저장 (새로 추가)
);
```

### 4. slack_user_mapping 테이블
```sql
CREATE TABLE slack_user_mapping (
    id UUID PRIMARY KEY,
    slack_user_id VARCHAR(100) NOT NULL,
    slack_workspace_id UUID,
    user_id UUID REFERENCES user(id),
    workspace_member_id UUID,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

---

## 자주 사용하는 쿼리

### 1. Gmail 토큰 확인
```python
import psycopg2
import json

conn = psycopg2.connect('postgresql://robeings:robeings@localhost:5432/main_db')
cur = conn.cursor()

# Gmail 토큰 상태 확인
query = """
    SELECT 
        u.username,
        u.email,
        g.user_id,
        g.is_equipped,
        g.robeing_id,
        g.token_data,
        g.scopes
    FROM gmail_token g
    JOIN user u ON g.user_id = u.id
    ORDER BY g.created_at DESC
"""

cur.execute(query)
rows = cur.fetchall()

for row in rows:
    print(f"\nUser: {row[0]} ({row[1]})")
    print(f"  UUID: {row[2]}")
    print(f"  Equipped: {row[3]}")
    print(f"  Robeing: {row[4]}")
    
    token_data = row[5] if row[5] else {}
    if token_data and 'access_token' in token_data:
        access_token = token_data.get('access_token', '')
        if access_token.startswith('ya29'):
            print(f"  Token: 실제 OAuth 토큰 있음")
        else:
            print(f"  Token: 테스트 토큰")
    
    scopes = row[6] if row[6] else []
    if scopes:
        print(f"  Scopes: {len(scopes)}개")

cur.close()
conn.close()
```

### 2. Slack User ID를 UUID로 변환
```python
import uuid

def slack_id_to_uuid(slack_user_id):
    """Slack User ID를 일관된 UUID로 변환"""
    namespace = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')
    # 51123 매핑 API 호출로 UUID 조회
    return call_mapping_api(slack_user_id)

# 예시
slack_id = 'U091UNVE41M'
user_uuid = slack_id_to_uuid(slack_id)
print(f"Slack ID: {slack_id}")
print(f"UUID: {user_uuid}")
# 출력: b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0
```

### 3. 사용자별 Gmail 토큰 추가/업데이트
```python
import psycopg2
import json
import uuid

def add_gmail_token(slack_user_id, email, access_token, refresh_token):
    """Gmail 토큰 추가 또는 업데이트"""
    
    # Slack ID를 UUID로 변환
    namespace = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')
    # 51123 매핑 API를 통해 UUID 조회
    user_uuid = await get_uuid_from_mapping_api(slack_user_id)
    
    conn = psycopg2.connect('postgresql://robeings:robeings@localhost:5432/main_db')
    cur = conn.cursor()
    
    try:
        # 1. users 테이블에 사용자 추가 (없으면)
        cur.execute("""
            INSERT INTO user (id, email, username, created_at)
            VALUES (%s, %s, %s, NOW())
            ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email
        """, (user_uuid, email, slack_user_id))
        
        # 2. gmail_token 추가 또는 업데이트
        token_data = {
            "access_token": access_token,
            "refresh_token": refresh_token,
            "token_type": "Bearer"
        }
        
        cur.execute("""
            INSERT INTO gmail_token (user_id, token_data, is_equipped, robeing_id, created_at)
            VALUES (%s, %s::jsonb, true, 'rb8001', NOW())
            ON CONFLICT (user_id) DO UPDATE 
            SET token_data = EXCLUDED.token_data,
                updated_at = NOW()
        """, (user_uuid, json.dumps(token_data)))
        
        conn.commit()
        print(f"✅ Gmail 토큰 저장 완료: {email}")
        
    except Exception as e:
        conn.rollback()
        print(f"❌ 오류 발생: {e}")
    finally:
        cur.close()
        conn.close()
```

---

## 문제 해결

### 1. SSH 터널이 연결되지 않을 때
```bash
# 기존 터널 프로세스 종료
sudo pkill -f "ssh.*5433"

# 포트 사용 확인
lsof -i :5433

# 다시 연결
ssh -f -N -L 5433:localhost:5432 admin@124.55.18.179 -p 51123
```

### 2. psycopg2 설치 오류
```bash
# Ubuntu/Debian
sudo apt-get install libpq-dev python3-dev
pip install psycopg2

# 또는 바이너리 버전 사용
pip install psycopg2-binary
```

### 3. UUID 타입 오류
```python
# UUID 문자열을 PostgreSQL UUID 타입으로 변환
import uuid

# 방법 1: 직접 캐스팅
cur.execute(
    "SELECT * FROM user WHERE id = %s::uuid",
    ('b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0',)
)

# 방법 2: uuid 객체 사용
user_uuid = uuid.UUID('b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0')
cur.execute(
    "SELECT * FROM user WHERE id = %s",
    (str(user_uuid),)
)
```

### 4. JSONB 필드 처리
```python
import json

# JSONB 데이터 삽입
data = {"key": "value", "nested": {"field": "data"}}
cur.execute(
    "INSERT INTO table_name (jsonb_column) VALUES (%s::jsonb)",
    (json.dumps(data),)
)

# JSONB 데이터 조회
cur.execute("SELECT jsonb_column FROM table_name")
row = cur.fetchone()
json_data = row[0]  # 자동으로 Python dict로 변환됨
```

---

## 환경 변수 설정

`.env` 파일에 추가:
```bash
# PostgreSQL Connection (51123 서버 직접 연결)
POSTGRES_CONNECTION_STRING=postgresql://robeings:robeings@localhost:5432/main_db

# PostgreSQL Connection (51124 서버 SSH 터널)
POSTGRES_CONNECTION_STRING=postgresql://robeings:robeings@localhost:5433/main_db

# SSH Tunnel Config
SSH_HOST=124.55.18.179
SSH_PORT=51123
SSH_USER=admin
LOCAL_PORT=5433
REMOTE_PORT=5432
```

Python에서 사용:
```python
import os
from dotenv import load_dotenv

load_dotenv()

conn_string = os.getenv('POSTGRES_CONNECTION_STRING')
conn = psycopg2.connect(conn_string)
```

---

## 자동화 스크립트

### SSH 터널 자동 재연결 스크립트
```bash
#!/bin/bash
# /home/heejae/scripts/ssh_tunnel_monitor.sh

LOCAL_PORT=5433
SSH_CMD="ssh -f -N -L 5433:localhost:5432 admin@124.55.18.179 -p 51123"

# 터널이 살아있는지 확인
if ! nc -z localhost $LOCAL_PORT 2>/dev/null; then
    echo "SSH tunnel is down. Restarting..."
    
    # 기존 프로세스 종료
    pkill -f "ssh.*$LOCAL_PORT"
    
    # 새 터널 생성
    $SSH_CMD
    
    echo "SSH tunnel restarted"
else
    echo "SSH tunnel is running"
fi
```

crontab에 추가 (5분마다 체크):
```bash
*/5 * * * * /home/heejae/scripts/ssh_tunnel_monitor.sh
```

---

## 참고 링크
- [PostgreSQL 공식 문서](https://www.postgresql.org/docs/)
- [psycopg2 문서](https://www.psycopg.org/docs/)
- [SSH 터널링 가이드](https://www.ssh.com/academy/ssh/tunneling)

---

---

## 서버별 DB 연결 상태 (2025-08-26 현재)

### 51123 서버 (192.168.219.45)
- **PostgreSQL**: 로컬 실행 중 (포트 5432)
- **데이터베이스**: main_db
- **직접 연결**: `postgresql://robeings:robeings@localhost:5432/main_db`

### 51124 서버 (192.168.219.52)
- **PostgreSQL**: 없음 (51123 서버 DB 사용)
- **연결 방법**: SSH 터널
- **터널 설정**: `ssh -N -L 5433:localhost:5432 admin@192.168.219.45`
- **연결 문자열**: `postgresql://robeings:robeings@localhost:5433/main_db`

### 주요 서비스별 DB 연결
| 서비스 | 서버 | DB 연결 방법 |
|--------|------|-------------|
| auth-server | 51123 | 직접 연결 (localhost:5432) |
| robeing-gateway | 51123 | 직접 연결 (localhost:5432) |
| rb8001 | 51124 | SSH 터널 (localhost:5433) |
| State Service | 51124 | SSH 터널 (localhost:5433) |
| skill-email | 51124 | SSH 터널 (localhost:5433) |

### 주의사항
- **auth_db는 더 이상 사용하지 않음** (main_db로 통합)
- State Service의 DATABASE_URL이 auth_db를 참조하면 main_db로 수정 필요
- SSH 터널은 시스템 재시작 시 재생성 필요

---

*마지막 업데이트: 2025-08-26*