sudo apt update
sudo apt install -y postgresql postgresql-contrib
sudo systemctl enable --now postgresql
상태 확인:
systemctl status postgresql --no-pager
postgres로 접속:sudo -iu postgres psql
CREATE DATABASE bootcamp_shop;
CREATE USER bootcamp WITH PASSWORD '1234';
GRANT ALL PRIVILEGES ON DATABASE bootcamp_shop TO bootcamp;
\q
sudo -u postgres psql -d bootcamp_shop -c "GRANT ALL ON SCHEMA public TO bootcamp;"
sudo -u postgres psql -d bootcamp_shop -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO bootcamp;"
sudo -u postgres psql -d bootcamp_shop -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO bootcamp;"
psql "host=localhost dbname=bootcamp_shop user=bootcamp password=1234"
PostgreSQL은 “네트워크 수신”과 “인증/접근 제어”를 분리해서 관리
→ 파일 2개를 반드시 설정해야 함
/etc/postgresql/*/main/postgresql.conf
/etc/postgresql/*/main/pg_hba.conf
postgresql.conf)기본값:
#listen_addresses = 'localhost'
외부 접속 허용:
listen_addresses = '*'
또는
listen_addresses = '0.0.0.0'
의미
'*': 모든 IPv4/IPv6 인터페이스'0.0.0.0': 모든 IPv4 인터페이스
pg_hba.conf)TYPE DATABASE USER ADDRESS METHOD
host all all 0.0.0.0/0 scram-sha-256
host all all 127.0.0.1/32 scram-sha-256
// 로컬 윈도우 주소
host bootcamp_shop bootcamp 192.168.80.1/32 scram-sha-256
PostgreSQL은 여기서 허용하지 않으면 절대 접속 불가
→
listen_addresses만 바꾸고 안 되는 이유의 90%
sudo systemctl restart postgresql
예: bootcamp_shop_pg.sql 생성:
cat > bootcamp_shop_pg.sql <<'SQL'
-- =========================================================
-- bootcamp_shop (PostgreSQL) Schema
-- =========================================================
-- (옵션) 실습 반복을 위한 초기화
DROP TABLE IF EXISTS payments CASCADE;
DROP TABLE IF EXISTS order_items CASCADE;
DROP TABLE IF EXISTS orders CASCADE;
DROP TABLE IF EXISTS products CASCADE;
DROP TABLE IF EXISTS customers CASCADE;
-- =========================================================
-- 1. customers
-- =========================================================
CREATE TABLE customers (
customer_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'SUSPENDED')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- =========================================================
-- 2. products
-- =========================================================
CREATE TABLE products (
product_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
category TEXT NOT NULL,
price NUMERIC(12,2) NOT NULL CHECK (price >= 0),
stock INTEGER NOT NULL CHECK (stock >= 0),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- =========================================================
-- 3. orders (order header)
-- =========================================================
CREATE TABLE orders (
order_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers(customer_id),
status TEXT NOT NULL DEFAULT 'CREATED'
CHECK (status IN ('CREATED', 'PAID', 'CANCELLED', 'REFUNDED')),
ordered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
total_amount NUMERIC(12,2) NOT NULL DEFAULT 0 CHECK (total_amount >= 0)
);
-- 인덱스(조회 빈도가 높은 컬럼)
CREATE INDEX idx_orders_customer_id_ordered_at
ON orders(customer_id, ordered_at DESC);
-- =========================================================
-- 4. order_items (order lines)
-- =========================================================
CREATE TABLE order_items (
order_item_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(order_id) ON DELETE CASCADE,
product_id BIGINT NOT NULL REFERENCES products(product_id),
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price NUMERIC(12,2) NOT NULL CHECK (unit_price >= 0),
-- PostgreSQL: 생성 컬럼(Generated Column)로 라인 금액 자동 계산 (MySQL과 문법/지원 차이 주의)
line_amount NUMERIC(12,2) GENERATED ALWAYS AS (quantity * unit_price) STORED,
-- 한 주문에서 같은 상품이 중복 라인으로 들어가는 것을 방지(정책에 따라 선택)
CONSTRAINT uq_order_items_order_product UNIQUE (order_id, product_id)
);
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE INDEX idx_order_items_product_id ON order_items(product_id);
-- =========================================================
-- 5. payments
-- =========================================================
CREATE TABLE payments (
payment_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(order_id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING', 'PAID', 'FAILED', 'REFUNDED')),
method TEXT NOT NULL DEFAULT 'CARD'
CHECK (method IN ('CARD', 'TRANSFER', 'VIRTUAL')),
amount NUMERIC(12,2) NOT NULL CHECK (amount >= 0),
paid_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- 일반적으로 "주문 1건당 결제 1건" 정책이라면 UNIQUE
CONSTRAINT uq_payments_order UNIQUE (order_id)
);
CREATE INDEX idx_payments_status_created_at ON payments(status, created_at DESC);
-- =========================================================
-- bootcamp_shop Sample Data (PostgreSQL)
-- =========================================================
-- 1) customers
INSERT INTO customers (customer_id, email, name, status, created_at) VALUES
(1, 'minsu@example.com', '김민수', 'ACTIVE', NOW() - INTERVAL '40 days'),
(2, 'jiyoon@example.com', '박지윤', 'ACTIVE', NOW() - INTERVAL '25 days'),
(3, 'seojun@example.com', '이서준', 'ACTIVE', NOW() - INTERVAL '10 days'),
(4, 'yuna@example.com', '최유나', 'SUSPENDED', NOW() - INTERVAL '5 days'),
(5, 'dohyuk@example.com', '정도혁', 'ACTIVE', NOW() - INTERVAL '2 days');
-- 2) products
INSERT INTO products (product_id, sku, name, category, price, stock, is_active, created_at) VALUES
(1, 'SKU-KEY-001', 'Mechanical Keyboard', 'INPUT', 119000.00, 50, TRUE, NOW() - INTERVAL '60 days'),
(2, 'SKU-MOU-002', 'Gaming Mouse', 'INPUT', 59000.00, 80, TRUE, NOW() - INTERVAL '60 days'),
(3, 'SKU-MON-003', '27inch Monitor', 'DISPLAY', 279000.00, 20, TRUE, NOW() - INTERVAL '45 days'),
(4, 'SKU-HDS-004', 'External SSD 1TB', 'STORAGE', 149000.00, 30, TRUE, NOW() - INTERVAL '30 days'),
(5, 'SKU-USB-005', 'USB-C Hub', 'ACCESS', 39000.00, 100, TRUE, NOW() - INTERVAL '20 days'),
(6, 'SKU-CAM-006', 'Webcam', 'ACCESS', 69000.00, 35, TRUE, NOW() - INTERVAL '15 days'),
(7, 'SKU-CHA-007', 'Office Chair', 'FURN', 189000.00, 10, TRUE, NOW() - INTERVAL '90 days'),
(8, 'SKU-MIC-008', 'USB Microphone', 'AUDIO', 99000.00, 25, TRUE, NOW() - INTERVAL '12 days');
-- 3) orders (총액은 일단 0으로 넣고, 아래에서 order_items 기준으로 갱신)
INSERT INTO orders (order_id, customer_id, status, ordered_at, total_amount) VALUES
(1001, 3, 'CREATED', NOW() - INTERVAL '1 day', 0),
(1002, 2, 'PAID', NOW() - INTERVAL '3 days', 0),
(1003, 1, 'PAID', NOW() - INTERVAL '8 days', 0),
(1004, 3, 'CANCELLED',NOW() - INTERVAL '15 days',0),
(1005, 5, 'CREATED', NOW() - INTERVAL '2 hours',0);
-- 4) order_items (unit_price는 “주문 시점 가격”으로 저장하는 패턴)
INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
(1001, 1, 1, 119000.00),
(1001, 5, 2, 39000.00),
(1002, 2, 1, 59000.00),
(1002, 4, 1, 149000.00),
(1003, 3, 1, 279000.00),
(1003, 6, 1, 69000.00),
(1003, 8, 1, 99000.00),
(1004, 5, 1, 39000.00),
(1005, 2, 2, 59000.00),
(1005, 1, 1, 119000.00);
-- 5) orders.total_amount 갱신 (order_items의 line_amount 합계)
UPDATE orders o
SET total_amount = t.sum_amount
FROM (
SELECT order_id, SUM(line_amount) AS sum_amount
FROM order_items
GROUP BY order_id
) t
WHERE o.order_id = t.order_id;
-- 6) payments (PAID 상태인 주문만 결제 생성)
INSERT INTO payments (payment_id, order_id, status, method, amount, paid_at, created_at) VALUES
(5001, 1002, 'PAID', 'CARD', (SELECT total_amount FROM orders WHERE order_id = 1002), NOW() - INTERVAL '3 days', NOW() - INTERVAL '3 days'),
(5002, 1003, 'PAID', 'TRANSFER', (SELECT total_amount FROM orders WHERE order_id = 1003), NOW() - INTERVAL '8 days', NOW() - INTERVAL '8 days');
-- 7) 재고 차감(샘플) - “PAID 주문만” 반영했다고 가정
UPDATE products p
SET stock = stock - x.qty
FROM (
SELECT oi.product_id, SUM(oi.quantity) AS qty
FROM order_items oi
JOIN orders o ON o.order_id = oi.order_id
WHERE o.status = 'PAID'
GROUP BY oi.product_id
) x
WHERE p.product_id = x.product_id;
-- 검증용 조회
SELECT * FROM customers ORDER BY customer_id;
SELECT * FROM products ORDER BY product_id;
SELECT * FROM orders ORDER BY order_id;
SELECT order_id, product_id, quantity, unit_price, line_amount
FROM order_items
ORDER BY order_id, product_id;
SELECT * FROM payments ORDER BY payment_id;
SQL
실행:
psql "host=localhost dbname=bootcamp_shop user=bootcamp password=1234" -f bootcamp_shop_pg.sql
psql 접속 후:
SELECT COUNT(*) FROM customers;
SELECT COUNT(*) FROM products;
SELECT COUNT(*) FROM orders;
SELECT COUNT(*) FROM order_items;
SELECT COUNT(*) FROM payments;
SELECT * FROM orders ORDER BY order_id;
SELECT
tc.table_name, kcu.column_name, ccu.table_name AS ref_table, ccu.column_name AS ref_column
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
ORDER BY tc.table_name, kcu.column_name;
orders 테이블의 status 허용값을 확인하라.order_items의 유니크 제약(한 주문에서 같은 상품 중복 방지)을 조회로 확인하라.payments가 “주문 1건당 결제 1건”을 어떻게 보장하는지 제약조건 기반으로 설명하라.힌트:
SELECT conname, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid = 'order_items'::regclass;