UNION / UNION ALL

UNION
- "테이블 결합"이라기보단 결과 결합
- SELECT 결과를 세로로 붙여서 하나로 만드는 것
- 열(컬럼) 수가 늘어나지 않는다. 대신 행(레코드)이 늘어난다.
- ex 1) 11월 신청자 목록, 12월 신청자 목록 따로 존재할 때
- ex 2) 웹 유입 로그와 앱 유입 로그가 따로 있고 '전체 유입'을 보고 싶을 때
- ex 3) '학생 목록'과 '강사 목록'을 합쳐서 '전체 사람 목록'을 만들고 싶을 때
- 규칙
● 각 SELECT는 같은 개수의 컬럼 반환해야 함
● 같은 위치의 컬럼까지 데이터 타입이 호환되어야 함
● 결과 컬럼명은 첫 번째 SELECT의 컬럼명/별칭 따라감
** 컬럼명 자체는 달라도 되지만, 결과의 '컬럼명'은 첫 번째 SELECT 기준으로 정해짐.
- UNION 결과 전체를 정렬하려면 맨 마지막에 ORDER BY를 둔다.
SELECT col1, col2 FROM table_a
UNION ALL
SELECT col1, col2 FROM table_b;
ORDER BY col1;
- 개별 SELECT에만 ORDER BY/LIMIT를 걸고 싶으면 괄호로 묶어야 한다. (서브 쿼리 활용)
UNION vs UNION ALL
- UNION
● 기본적으로 "중복 행 제거" (UNION = DISTINCT 기본)
● 기본 문법
SELECT col1, col2
FROM table_a
UNION
SELECT col1, col2
FROM table_b;
- UNION ALL
● 중복을 제거하지 않고 그대로 다 포함
● 기본 문법
SELECT col1, col2
FROM table_a
UNION ALL
SELECT col1, col2
FROM table_b;
# 체크포인트 퀴즈
Q1. UNION과 UNION ALL의 가장 큰 차이는?
- A) UNION은 열이 늘고, UNION ALL은 행이 늘어난다
- B) UNION은 중복 제거, UNION ALL은 중복 유지
- C) UNION ALL만 ORDER BY를 쓸 수 있다
답) B) UNION은 중복 제거, UNION ALL은 중복 유지
해설) A) UNION, UNION ALL 모두 행이 늘어난다.
C) UNION, UNION ALL 모두 ORDER BY를 쓸 수 있다.
Q2. UNION이 성립하려면 어떤 조건이 필요한가?
- A) 두 테이블의 컬럼명이 완전히 같아야 한다
- B) 각 SELECT가 같은 개수의 컬럼을 반환하고, 같은 위치의 타입이 호환되어야 한다
- C) PK/FK가 설정되어 있어야 한다
답) B) 각 SELECT가 같은 개수의 컬럼을 반환하고, 같은 위치의 타입이 호환되어야 한다.
해설) A) 두 테이블의 컬럼명이 완전히 같을 필요 없다. 컬럼명 자체는 달라도 되지만, 결과의 '컬럼명'은 첫 번째 SELECT 기준으로 정해진다.
C) UNION은 서로 아무런 관계가 없는 테이블끼리도 수행할 수 있다. 서로 PK/FK로 연결되어 있지 않지만, 형식이 같다면 하나로 합쳐서 보고 싶을 수 있다. PK(기본키)나 FK(외래키)는 데이터의 무결성을 지키기 위한 제약 조건일 뿐, 데이터를 조회하고 합치는 연산에는 영향을 주지 않는다.
JOIN
- 옆으로 붙이기
- 관계(카디널리티) 감각 1:1 / 1:N이 중요한 이유
- "문법"이 아니라 결과가 늘어나는 방식이기 때문에 JOIN이 어려움.
ex 1) 학생 1명은 수강신청 여러 번 가능 -> JOIN하면 학생 행 증가 가능
ex 2) 1개 신청에 결제가 여러 번 찍힐 수 있음(재결제/부분환불/분할결제 등) -> JOIN하면 더 늘어날 수 있음
- 그래서 어떻게? (HOW?)
● 먼저 원하는 최종 행 단위(grain 선언)
● N쪽 테이블은 'JOIN 전에' 집계/선택 후 1행으로 만들어 붙이기
● 최종 테이블 확인
- JOIN 전후 row count 비교
- 키 중복 여부 확인
- JOIN의 성패는 3단계로 결정남.
● 목적
- 모든 테이블에 대한 이해(정보) 필요
- 최종 결과가 무엇을 보고 싶은지 스스로에게 질문
● 공통 컬럼 찾기
- JOIN은 테이블을 연결하는 '고리(키)' 필요
- 보통 이 고리는 ID 컬럼
● PK/FK 관계 이해
▼ PK (Primmary Key, 기본키)
● 한 테이블에서 각 행을 유일하게 식별하는 컬럼
● 중복, NULL이면 안 됨
▼ FK (Foreign Key, 외래키)
● 다른 테이블의 PK 참조하는 칼럼
● 우리 테이블의 이 행이 누구/무엇과 연결되는지 알려주는 값
** 실제 현업에서는 PK/FK 이미 설정되있는 경우 대다수, 데이터 분석가는 그 관계를 이해한 후 JOIN 조건(ON)에 쓰는 역할
● 적절한 JOIN 방식 선택
종류 & 기본 문법(패턴)

- 기본 문법
SELECT
a.col1,
b.col2
FROM table_a AS a
JOIN table_b AS b
ON a.key= b.key;
- 종류
● INNER JOIN (교집합)

- 양쪽에 모두 존재하는 것만 보고 싶을 때
- 즉, 두 테이블에서 일치하는 값을 가진 행만 반환
SELECT
a.key, a.col A, b.col B
FROM A a
INNER JOIN B b
ON a.key= b.key;
● LEFT JOIN (왼쪽 기준 + 매칭 없으면 NULL)

- 기준(모수)을 유지하고 싶을 때
- 왼쪽 테이블의 모든 행 + 오른쪽 테이블에서 일치하는 행 반환.
- 일치하지 않으면 오른쪽 컬럼은 NULL. 이를 이용해 '상대 테이블에 없는 대상'을 찾을 수 있음.
- 실무에서 많이 사용됨.
SELECT
a.key, a.col A, b.col B
FROM A a
LEFT JOIN B b
ON a.key= b.key;
● RIGHT JOIN (오른쪽 기준)
- LEFT JOIN과 유사
- 오른쪽 테이블의 모든 행 + 왼쪽 테이블에서 일치하는 행 반환
- 일치하지 않으면 왼쪽 컬럼은 NULL
- 이식성(portability)을 위해 LEFT JOIN 사용 권장
- 실제 작성 LEFT JOIN 통일 권장. -> 기준 테이블만 달라짐.
ON vs WHERE (LEFT JOIN에서 자주 터지는 함정)
- ON : 조인을 "어떻게 붙일지" 조건
- WHERE : 결과에서 "어떤 행을 남길지" 필터
** LEFT JOIN 후 WHERE 때문에 INNER JOIN처럼 되는 현상 발생.
- LEFT JOIN 후 WHERE 사용 시 NULL이 WHERE에서 걸러져 INNER JOIN과 같은 결과 출력
=> LEFT JOIN 유지, 조건을 ON에 둔다면 LEFT JOIN 결과 출력
# 미니 실습
문제 1
상황
운영팀이 “수강신청 리스트(enrollments)”를 보는데, course_id만 있으면 사람이 읽기 불편해서 강좌명/카테고리를 붙여 달라고 합니다.
목표
enrollments와 courses를 INNER JOIN해서 아래를 출력하세요.
출력 컬럼
- enrollment_id
- course_id
- course_name
- category
- final_price
조건/정렬
- 조건: enrollment_status = 'active' 인 신청만
- 정렬: enroll_date 오름차순
답)
select
e.enrollment_id,
e.course_id,
c.course_name,
c.category,
e.final_price
from basic.enrollments as e
inner join basic.courses as c
on e.course_id = c.course_id
where e.enrollment_status = 'active'
order by enroll_date desc;
해설)
select
e.enrollment_id,
e.course_id,
c.course_name,
c.category,
e.final_price
from basic.enrollments as e
inner join basic.courses as c
on e.course_id = c.course_id
where e.enrollment_status = 'active'
order by enroll_date asc;
=> 오름차순은 asc이다. desc는 내림차순이다. 헷갈리지 않도록 해야한다. asc/desc 주의 요망..
문제 2
상황
CS팀이 수강신청(enrollments) 목록을 기준으로,
- 결제가 성공(paid)한 신청이면 결제 정보를 보여주고
- 결제 성공 기록이 없으면 “결제 정보 칸이 비어있게(NULL)” 보여주고 싶어합니다.
즉, 수강신청은 전부 보여야 하므로 enrollments가 기준(LEFT) 입니다.
목표
enrollments e를 기준으로 payments p를 LEFT JOIN해서 아래 컬럼을 출력하세요.
출력 컬럼
- e.enrollment_id
- e.student_id
- e.course_id
- e.enroll_date
- e.final_price
- p.payment_id (결제 성공이 없으면 NULL)
- p.paid_at (결제 성공이 없으면 NULL)
- p.amount (결제 성공이 없으면 NULL)
조건
- 결제 정보는 payment_status = 'paid'인 것만 붙이기
- 단, 결제 성공이 없어도 enrollments 행은 사라지면 안 됨
정렬
- e.enroll_date ASC, e.enrollment_id ASC, p.paid_at ASC
참고
- 어떤 신청(enrollment)은 결제가 여러 번 있을 수 있습니다. 그러면 JOIN 결과에서 그 신청은 여러 행으로 보이는 게 정상입니다(1:N).
답)
select
e.enrollment_id,
e.student_id,
e.course_id,
e.enroll_date,
e.final_price,
p.payment_id,
p.paid_at,
p.amount
from basic.enrollments as e
left join basic.payments as p
on e.enrollment_id = p.enrollment_id
where p.payment_status = 'paid'
order by e.enroll_date asc, e.enrollment_id asc, p.paid_at asc
다중 JOIN + 결과 검증 루틴
- (A와 B를 먼저 붙인 결과) 에 C를 또 붙이는 것 -> A JOIN B JOIN C = JOIN 2번 하는 것
- JOIN 한 번 할때마다 필수 확인 사항
● 왜 row 수가 변했지? (1:N이면 늘어나는게 정상)
● 내가 보고 싶은 단위(레벨)가 뭐였지?
** 1:N 관계면 JOIN 결과 행이 늘어나는 건 정상이라는 설명은 일반적인 조인 개요에서도 반복
# 미니 실습
문제 1
상황
운영팀이 강좌(courses) 별로
- 신청이 몇 건인지(신청 수)
- 결제 성공(paid) 금액이 얼마인지(매출)를 보고 싶어합니다.
사용 테이블(3개)
- courses c
- enrollments e
- payments p
조인 규칙
- c.course_id = e.course_id
- e.enrollment_id = p.enrollment_id
- 결제는 payment_status='paid'인 것만 매출로 인정 (이 조건은 ON에 두는 걸 추천)
출력 컬럼
- course_id
- course_name
- enroll_cnt = 신청 건수 (COUNT(DISTINCT e.enrollment_id))
- paid_revenue = 결제 성공 금액 합계 (SUM(p.amount))
- paid_payment_rows = 결제 성공 결제 이벤트 수 (COUNT(p.payment_id))
조건/정렬
- 조건: c.is_active = 1 인 강좌만
- 정렬: paid_revenue DESC, course_name ASC
답)
select
c.course_id,
c.course_name,
count(distinct e.enrollment_id) as enroll_cnt,
sum(p.amount) as paid_revenue,
count(p.payment_id) as paid_payment_rows
from basic.courses as c
left join basic.enrollments as e
on c.course_id = e.course_id
left join basic.payments as p
on e.enrollment_id = p.enrollment_id
where payment_status='paid'
group by c.course_id, c.course_name
order by paid_revenue desc, course_name asc;
해설)
SELECT
c.course_id,
c.course_name,
COUNT(DISTINCT e.enrollment_id) AS enroll_cnt,
SUM(p.amount) AS paid_revenue,
COUNT(p.payment_id) AS paid_payment_rows
FROM basic.courses c
LEFT JOIN basic.enrollments e
ON c.course_id = e.course_id
LEFT JOIN basic.payments p
ON e.enrollment_id = p.enrollment_id
AND p.payment_status = 'paid'
WHERE c.is_active = 1
GROUP BY c.course_id, c.course_name
ORDER BY paid_revenue DESC, c.course_name ASC
=> LEFT JOIN에서 조건을 WHERE에 적게되면 먼저 걸러지게 되어서 ON에 조건을 담아야한다. 그러므로 enrollments 테이블과 payments 테이블의 LEFT JOIN문에서 ON에서 AND로 조건을 모두 걸어줘야한다. 그 후 총 3개의 테이블에 모두 걸리는 조건은 WHERE에 작성하면 된다.
UNION vs JOIN + 함께 쓰는 패턴
- UNION : 결과를 세로로 합침 (행이 늘어남) - set operation
- JOIN : 테이블을 가로로 붙임 (열이 늘어남) - join clause
- MySQL은 FULL OUTER JOIN을 지원하지 않는다.
● LEFT JOIN 결과 + RIGHT JOIN 결과 UNION해 흉내냄. (매칭되는 행 중복은 UNION이 제거)
# 데일리 과제 3
과제 1 — 전체 사람 목록(학생+강사) 만들기
✅ 상황
운영팀이 공지 발송을 위해 “사람 목록”이 필요합니다.
학생(students) 과 강사(instructors) 를 한 결과로 합쳐서(세로로 쌓아서) 보고 싶습니다.
- 이름이 같은 사람이 있을 수도 있으니(동명이인) 중복 제거를 하면 안 됩니다.
✅ 목표
학생/강사 데이터를 아래 컬럼으로 통일해서 한 결과로 합치기
출력 컬럼
- person_type : 학생이면 'student', 강사면 'instructor'
- person_id : 학생은 student_id, 강사는 instructor_id
- person_name : 학생은 student_name, 강사는 instructor_name
정렬
- person_type ASC, person_id ASC
답)
select
'student' as person_type,
student_id as person_id,
student_name as person_name
from basic.students
union all
select
'instructor' as person_type,
instructor_id as person_id,
instructor_name as person_name
from basic.instructors
order by person_type asc, person_id asc
과제 2 — 카테고리별 진행중(active) 신청 현황 확인하기
✅ 상황
운영팀이 “어떤 카테고리가 신청이 잘 들어오는지” 보고 싶어합니다.
그런데 enrollments에는 category가 없고, courses에만 있습니다.
→ 수강신청(enrollments) 과 강좌(courses) 를 course_id로 INNER JOIN해서 카테고리를 붙인 뒤 집계합니다.
✅ 목표
enrollment_status = 'active' 인 신청만 대상으로, 카테고리별 지표를 뽑으세요.
출력 컬럼
- category
- active_enrollments : 진행중 신청 건수 (COUNT(*))
- unique_students : 진행중 신청한 “고유 학생 수” (COUNT(DISTINCT e.student_id))
- avg_final_price : 진행중 신청의 평균 결제대상금액 (ROUND(AVG(e.final_price), 0))
조건
- enrollment_status = 'active'
추가 조건 (그룹 필터)
- active_enrollments >= 2 인 카테고리만 남기기 (HAVING)
정렬
- active_enrollments DESC, category ASC
답)
select
c.category,
count(*) as active_enrollments,
count(distinct e.student_id) as unique_students,
round(avg(e.final_price),0) as avg_final_price
from basic.enrollments as e
inner join basic.courses as c
on e.course_id = c.course_id
where enrollment_status = 'active'
group by c.category
having active_enrollments >=2
order by active_enrollments desc, category asc
과제 3 — 결제 성공(paid) 기록이 없는 신청 찾기
✅ 상황
CS팀이 “결제 성공이 안 된 신청건”을 찾아서 안내 메시지를 보내려 합니다.
포인트는 아래와 같습니다.:
- 기준(모수)은 수강신청(enrollments) 입니다 → 신청은 일단 존재하니까
- 결제 성공(payment_status='paid')이 있으면 결제 정보를 붙이고
- 결제 성공이 없으면 결제 컬럼이 NULL로 남게 만들고(LEFT JOIN)
- 그 NULL을 이용해 결제 성공이 없는 신청만 골라냅니다.
✅ 목표
결제 성공(paid) 기록이 없는 신청만 출력하세요.
출력 컬럼
- e.enrollment_id
- e.student_id
- e.course_id
- e.enroll_date
- e.enrollment_status
- e.final_price
조건
- 신청 상태는 active, completed만 포함 (취소 cancelled 제외)
- 결제는 paid만 “성공”으로 인정
- 이 조건은 WHERE가 아니라 ON에 넣는 게 핵심!
정렬
- e.enroll_date ASC, e.enrollment_id ASC
답)
select
e.enrollment_id,
e.student_id,
e.course_id,
e.enroll_date,
e.enrollment_status,
e.final_price
from basic.enrollments as e
left join basic.payments as p
on e.enrollment_id = p.enrollment_id
and payment_status='paid'
where enrollment_status in('active', 'completed') and p.enrollment_id is null
order by e.enroll_date asc, e.enrollment_id asc
=> 첫번째 조건인 active, completed만 포함시킬 때 IN(컬럼1, 컬럼2) 를 사용해야 한다.
=> 'NULL을 이용해 결제 성공이 없는 신청만' 골라낼 때 WHERE문에 AND를 통해 IS NULL로 조건을 걸어줘야 한다.
과제 4 — 강좌별 신청수 + 결제매출 리포트
✅ 상황
운영팀이 강좌별로 아래를 보고 싶어합니다.
- 신청이 얼마나 들어왔는지(신청 수)
- 결제 성공 매출이 얼마인지(매출)
- 결제 이벤트가 몇 번 발생했는지(결제 rows)
사용 테이블 (3개)
- courses c (1행=강좌 1개)
- enrollments e (1행=수강신청 1건)
- payments p (1행=결제 이벤트 1건)
JOIN 규칙
- c.course_id = e.course_id
- e.enrollment_id = p.enrollment_id
- 매출은 p.payment_status = 'paid' 인 결제만 인정 (이 조건은 ON에 두기)
출력 컬럼
- c.course_id
- c.course_name
- enroll_cnt : 신청 건수 (COUNT(DISTINCT e.enrollment_id))
- paid_enroll_cnt : 결제 성공 이력이 있는 신청 수 (COUNT(DISTINCT p.enrollment_id))
- paid_payment_rows : 결제 성공 결제 이벤트 수 (COUNT(p.payment_id))
- paid_revenue : 결제 성공 금액 합 (SUM(p.amount))
- paid_enroll_rate_pct : paid_enroll_cnt / enroll_cnt * 100 (소수점 1자리)
조건
- c.is_active = 1 (운영중 강좌만)
주의(중요)
- 신청이 1건도 없으면 enroll_cnt=0이어서 비율 계산이 애매해집니다.
- → 이번 과제에서는 신청이 1건 이상인 강좌만 포함시키세요. (HAVING)
정렬
- paid_revenue DESC, c.course_name ASC
답)
select
c.course_id,
c.course_name,
count(distinct e.enrollment_id) as enroll_cnt,
count(distinct p.enrollment_id) as paid_enroll_cnt,
count(p.payment_id) as paid_payment_rows,
sum(p.amount) as paid_revenue,
round(
count(distinct p.enrollment_id)
/ count(distinct e.enrollment_id) * 100, 1) as paid_enroll_rate_pct
from basic.courses as c
left join basic.enrollments as e
on c.course_id = e.course_id
left join basic.payments as p
on e.enrollment_id = p.enrollment_id
and p.payment_status = 'paid'
where c.is_active = 1
group by c.course_id, c.course_name
having enroll_cnt >=1
order by paid_revenue desc, c.course_name asc
# 데일리 퀴즈 3
Q1. UNION과 UNION ALL의 가장 큰 차이는?
A. UNION은 가로 결합, UNION ALL은 세로 결합이다
B. UNION은 중복 행 제거, UNION ALL은 중복 행 유지다
C. UNION은 ORDER BY를 못 쓰고, UNION ALL은 ORDER BY를 쓸 수 있다
D. UNION ALL은 PK/FK가 있어야만 가능하다
답) B. UNION은 중복 행 제거, UNION ALL은 중복 행 유지다.
해설) A. UNION, UNION ALL 모두 세로 결합이다.
C. UNION, UNION ALL 모두 ORDER BY 쓸 수 있다.
D. UNION ALL은 PK/FK가 없어도 가능하다.
Q2. UNION으로 “전체 결과”를 정렬하려면 ORDER BY는 어디에 써야 할까요?
A. 첫 번째 SELECT 안에 쓴다
B. 각 SELECT마다 ORDER BY를 각각 쓴다
C. UNION으로 연결된 마지막 SELECT 뒤에 쓴다
D. UNION에서는 ORDER BY를 쓸 수 없다
답) C. UNION으로 연결된 마지막 SELECT 뒤에 쓴다.
해설) D. UNION에서는 ORDER BY를 쓸 수 있다.
Q3. UNION을 사용할 때 필요한 조건으로 올바른 것은?
A. 두 SELECT의 컬럼 이름이 완전히 같아야 한다
B. 각 SELECT가 반환하는 컬럼 개수는 달라도 된다
C. 각 SELECT는 같은 개수의 컬럼을 반환해야 한다
D. 두 테이블 사이에 PK/FK가 반드시 설정돼 있어야 한다
답) C. 각 SELECT는 같은 개수의 컬럼을 반환해야 한다.
해설) A. 두 SELECT의 컬럼 이름이 완전히 같을 필요 없다.
B. 각 SELECT가 반환하는 컬럼 개수는 같아야 한다.
D. 두 테이블 사이에 PK/FK가 없어도 된다.
Q4. LEFT JOIN에서 오른쪽 테이블에 매칭되는 행이 없으면 어떤 일이 발생하나요?
A. 왼쪽 행도 함께 사라진다
B. 왼쪽 행은 남고, 오른쪽 컬럼들이 NULL로 채워진다
C. 에러가 발생한다
D. 자동으로 RIGHT JOIN으로 바뀐다
답) B. 왼쪽 행은 남고, 오른쪽 컬럼들이 NULL로 채워진다.
해설) A. 왼쪽 행은 그대로다.
C. 에러가 발생하지 않는다.
D. 자동으로 RIGHT JOIN으로 바뀌지 않는다.
Q5. 아래 쿼리가 “LEFT JOIN을 했는데도 결제 없는 신청이 사라지는” 이유로 가장 적절한 것은?
SELECT e.enrollment_id, p.payment_status
FROM basic.enrollments e
LEFT JOIN basic.payments p
ON e.enrollment_id= p.enrollment_id
WHERE p.payment_status='paid';
A. LEFT JOIN은 원래 결제 없는 신청을 못 가져온다
B. WHERE 조건이 오른쪽 NULL 행을 제거해서 결과가 INNER JOIN처럼 된다
C. payment_status는 숫자형이 아니라서 비교가 안 된다
D. ON에는 조건을 쓸 수 없어서 에러가 난다
답) B. WHERE 조건이 오른쪽 NULL 행을 제거해서 결과가 INNER JOIN처럼 된다.
해설) A. LEFT JOIN은 원래 결제 없는 신청도 가져올 수 있다.
C. payment_status는 숫자형이 아니라도 같다, 같지 않다 로 비교 가능하다.
D. ON에는 조건을 쓸 수 있다.
팀 진행 및 결정사항
2025.12.29 결정사항
- 1월2일 금요일 아티클 “개발 블로그는 어떻게 써야할까? (f-lab)”
내일 할 일
1. SQL 5주차 강의 정리
2. SQL 5주차 실전문제 풀기
3. SQL 데일리세션 정리 및 과제
4. SQL 코드카타
오늘의 회고
스파르타에서 먼저 제공해 준 강의를 듣고 라이브세션을 들으니 좀 덜 어려웠던것 같다. 하지만 JOIN은 JOIN이다. 이해에 많은 시간을 필요로 했다. 그래도 데일리 과제가 사소한 오류가 있었지만 큰 문제없이 해결했다. 지난 일차 대비 문제 자체는 시간이 덜 든 것 같다. 튜터님의 말대로 데이터를 이해해야 풀기 편한 것 같다. 지금 과제는 문제가 매우 친절한 편이다. 열심히 노력하겠다. 내일은 SQL 코드카타가 시작된다. 이것조차 잘 해내겠지..?
'내일배움캠프' 카테고리의 다른 글
| [내일배움캠프] TIL 7일차 25.12.31(수) (0) | 2025.12.31 |
|---|---|
| [내일배움캠프] TIL 6일차 25.12.30(화) (2) | 2025.12.30 |
| [내일배움캠프] TIL 4일차 25.12.26(금) (0) | 2025.12.26 |
| [내일배움캠프] TIL 3일차 25.12.24(수) (0) | 2025.12.24 |
| [내일배움캠프] TIL 2일차 25.12.23(화) (1) | 2025.12.23 |