
혼자 공부하는 SQL
✔️ MySQL 데이터 형식
✔️ 테이블끼리 연결하는 JOIN
✔️ SQL 프로그래밍
✔️ MySQL 데이터 형식
실제로 저장될 데이터의 형태가 다양하기 때문에
각 데이터에 맞는 데이터 형식을 지정하여 저장
예) 숫자형, 문자형, 날짜형 등
# 정수형
소수점이 없는 숫자
예) 인원 수, 가격, 수량 등
정수형의 데이터 형식과 표현
TITNYINT : 127숫자 표현
SMALLINT : 32767숫자 표현
INT : 21억 숫자 표현
BIGINT : 900경 숫자 표현
정수형 테이블 생성
-- 테이블 생성 후 데이터 입력
use market_db;
create table new_table (
tinyint_col tinyint,
smallint_col smallint,
int_col int,
bigint_col bigint
);
-- 열의 최대값 입력 수행
insert into new_table values(127, 32767, 214783647, 9000000000000000000);
-- 범위 이상의 값을 저장하려 한다면 Out of range 오류 발생
-- Out of range 오류 : 입력값의 범위를 벗어났다는 의미
insert into new_table values(128, 32768, 214783648, 90000000000000000000);

정수형 사용 예제(member 테이블)
mem_number는 127명 이상인 경우는 없기 때문에 TINYINT가 효율적
height는 TINYINT 범위(-128 ~ 127)가 부족하지만 값의 범위가 0부터 시작되는 UNSIGNED 예약어 활용
TINYINT는 -128 ~ 127, TINYINT UNSIGNED는 0 ~ 256 범위를 나타내면 모두 256개를 표현하는 것이다.
-- 기존의 테이블 구성
CREATE TABLE member
( mem_id CHAR(8) NOT NULL PRIMARY KEY,
mem_name VARCHAR(10) NOT NULL,
mem_number INT NOT NULL, -- INT(정수형)
addr CHAR(2) NOT NULL,
phone1 CHAR(3),
phone2 CHAR(8),
height SMALLINT, -- SMALLINT(정수형)
debut_date DATE
);
-- 효율적 테이블 구성
CREATE TABLE member
( mem_id CHAR(8) NOT NULL PRIMARY KEY,
mem_name VARCHAR(10) NOT NULL,
mem_number TINYINT NOT NULL, -- TINYINT(정수형)
addr CHAR(2) NOT NULL,
phone1 CHAR(3),
phone2 CHAR(8),
height TINYINT UNSIGNED, -- TINYINT UNSIGNED(정수형)
debut_date DATE
);
# 문자형
글자를 저장하기 위해 사용, 입력할 최대 글자의 수 지정
문자형의 데이터 형식과 표현
CHAR(개수) : 1 ~ 255
VACHAR(개수) : 1 ~ 16383
CHAR(개수)
고정길이 문자형(자릿수가 고정되어 있음)
VARCHAR()보다 내부적으로 성능이 빠르기 때문에 용이
(글자수가 지정되어 있는 경우는 CHAR() 용이)
예) CHAR(10)에 '가나다' 저장
10개의 공간을 모두 확보하고 3공간을 사용하여 '가나다' 입력
7자리는 낭비하게 된다
VARCHAR(개수)
가변길이 문자형(자릿수가 고정되어 있지 않음)
(여러 자리의 글자수가 오는 경우(가수 이름 등) VARCHAR()이 용이)
예) VARCHAR(10)에 '가나다' 저장
'가나다'를 3자리에 저장해야 한다면 3자리만 확보하여 저장
-- 기존의 테이블 구성
CREATE TABLE member
( mem_id CHAR(8) NOT NULL PRIMARY KEY,
mem_name VARCHAR(10) NOT NULL,
mem_number INT NOT NULL, -- INT(정수형)
addr CHAR(2) NOT NULL,
phone1 CHAR(3),
phone2 CHAR(8),
height SMALLINT, -- SMALLINT(정수형)
debut_date DATE
);
-- 효율적인 테이블 설정을 위한 문자형 확인
CREATE TABLE member
( mem_id CHAR(8) NOT NULL PRIMARY KEY, -- 더 긴 회원아이디 생성 가정
mem_name VARCHAR(10) NOT NULL,
mem_number TINYINT NOT NULL, -- TINYINT(정수형)
addr CHAR(2) NOT NULL,
phone1 CHAR(3), -- 정수형으로 저장하면 앞 0이 사라지기 때문
phone2 CHAR(8), -- 숫자로서의 의미 < 문자로서의 의미(전화번호)
height TINYINT UNSIGNED, -- TINYINT UNSIGNED(정수형)
debut_date DATE
);
+
대량의 데이터 형식

BLOB : 이미지, 동영상 등의 데이터
만약 넷플릭스가 DB를 생성하여 movie 테이블을 생성한다면?
create database netflix_db;
use netflix_db;
CREATE TABLE movie (
movie_id int,
movie_title varchar(30),
movie_director varchar(20),
movie_star varchar(20),
movie_script LONGTEXT, -- 자막(LONGTEXT로 지정 -> 최대 4G 저장)
movie_film LONGBLOB -- LONGBLOB : 대용향의 텍스트와 이진 데이터를 저장 가능(최대 4G 저장)
);
# 실수형
소수점이 있는 숫자 저장 시 활용

FLOAT와 DOUBLE는 거의 비슷하다(소수점 아래 자리수만 다름) -> 대부분 FLOAT 사용
# 날짜형
날짜 및 시간 저장 시 활용

DATE(날짜만 저장), TIME(시간만 저장) = DATETIME(날짜와 시간 전부 저장)
# 변수의 선언과 대입
MySQL 워크벤치 재시작할때까지는 유지되지만 종료하면 사라짐(임시로 사용)
use market_db;
-- 변수를 선언하고 정수 또는 실수 대입
set @myVar1 = 5;
set @myVar2 = 4.25;
-- 변수의 내용 출력
select @myVar1;
-- 변수끼리 연산한 후 출력
select @myVar1 + @myVar2;


변수 활용 예시
-- 변수를 선언하고 문자열 또는 정수 대입
set @txt = '가수 이름 => ';
set @height = 166;
-- 테이블 조회하는데 변수 활용
select
@txt,
mem_name
from member
where height > @height; -- height > 166과 동일

LIMIT에 변수 활용
PREPARE과 EXECUTE 활용하여 오류 해결
PREPARE : SELECT문을 준비만 수행
EXECUTE : 실행하는 부분
-- LIMIT에 활용
set @count = 3;
-- LIMIT @count 오류 발생
select
mem_name,
height
from member
order by height;
-- 오류 해결
PREPARE MySQL
from 'select mem_name, height from member order by height limit ?';
EXECUTE MySQL using @count;

# 데이터 형 변환
문자형 -> 정수형, 정수형 -> 문자형으로의 변환 의미
명시적인 변환 : 직접 함수를 사용한 변환
암시적인 변환 : 별도의 지시 없이 자연스러운 변환
명시적 변환
-- 명시적 변환의 기본 형식
CAST(값 as 데이터형식[(길이)])
CONVERT(값 as 데이터형식[(길이)])
-- 예시 확인
select
avg(price) as '평균 가격'
from buy;
-- 평균 가격을 정수형 형식으로 표현(CAST 활용)
select
cast(avg(price) as signed) as '평균 가격'
from buy;
-- 평균 가격을 정수형 형식으로 표현(CONVERT 활용)
select
convert(avg(price), signed) as '평균 가격'
from buy;

날짜와 원하는 형태로 결과 표현하는 경우도 확인
-- 다양한 구분자를 날짜형으로 변환
select
cast('2022$12$12' as date),
cast('2022/12/12' as date),
cast('2022%12%12' as date),
cast('2022@12@12' as date);
-- 결과를 원하는 형태로 변환
select
num,
concat(cast(price as char), 'X', cast(amount as char), '=') '가격X수량',
price * amount as '구매액'
from buy;


암시적 변환
-- 문자형 100과 200 더함
select '100' + '200';
-- 만약 문자를 연결하려면 CONCAT 활용
select concat('100','200');
-- 숫자와 문자를 섞어서 사용한다면?
-- 숫자 100과 문자 200을 더하면 자동 변환되어 300 나옴
select concat(100, '200');
select 100 + '200';


✔️ 테이블끼리 연결하는 JOIN
# JOIN(조인)
2개의 테이블을 서로 묶어서 하나의 결과를 도출
두 개의 테이블을 엮어야만 원하는 결과가 나오는 경우 사용됨
# 내부 조인
데이터베이스의 테이블들 중 일대다 관계가 성립되어야 JOIN 사용 가능
-- 내부 조인의 기본 형식
select
열 이름1,
열 이름2,
...
from 첫번째 테이블
inner join 두번째 테이블 on 조인조건 -- join만 써도 inner join으로 인식
[where 검색 조건];
일대다 관계
한쪽 테이블에는 하나의 값만 존재햐야 하지만 연결된 다른 테이블에는 여러 개의 값 존재 가능한 관계
예) market_db의 member 테이블과 buy 테이블
member 테이블의 mem_id는 독립적으로 한개씩 존재, 이를 기본키(primary key, PK)로 지정하고,
buy(구매) 테이블의 mem_id는 여러 번 존재함(한 회원이 여러 번 구매 가능)
buy 테이블의 mem_id는 외래키(foreeign key, FK)로 지정하여 JOIN 수행 가능
buy 테이블과 member 테이블의 모든 열이 결과로 나타남
use market_db;
-- JOIN 수행
select
*
from buy as b
join member as m
on b.mem_id = m.mem_id -- 별칭으로 적용 가능
where b.mem_id = 'GRL';


where절을 생략한다면 테이블의 모든 행에 대한 결과가 나옴
select
*
from buy as b
join member as m
on b.mem_id = m.mem_id; -- 별칭으로 적용 가능

내부 조인의 간결한 표현
mem_id, mem_name, prod_name, addr, concat(phone1, phone2)에 대해서 확인
-- 오류 발생
-- mem_id가 어느 테이블의 mem_id인지 명확하지 않음
select
mem_id,
mem_name,
prod_name,
addr,
concat(phone1, phone2) as '연락처'
from buy as b
join member as m on b.mem_id = m.mem_id;
-- buy 테이블의 mem_id를 기준으로 다시 추출
-- 위 쿼리와 동일하게 별칭을 사용하여 적용하면 간결하게 쿼리 작성 가능
select
b.mem_id,
mem_name,
prod_name,
addr,
concat(phone1, phone2) as '연락처'
from buy as b
join member as m on b.mem_id = m.mem_id;

중복된 결과 1개만 출력(DISTINCT 활용)
select distinct
m.mem_id,
m.mem_name,
m.addr
from buy as b
join member as m on b.mem_id = b.mem_id
order by m.mem_id;

+
내부 조인의 한계
전체 회원의 mem_id, mem_name, prod_name, addr을 기준으로 회원 아이디 순으로 정렬
select
m.mem_id,
m.mem_name,
b.prod_name,
m.addr
from buy as b
join member as m on b.mem_id = m.mem_id
order by m.mem_id;

해당 결과를 봤을 때 '전체 회원'이 의미하는 바는 한 번도 구매하지 않은 회원의 정보도 포함되어야 하는데
두 테이블의 모두 있는 내용만이 JOIN되는 방식이다 보니 전체 회원의 정보는 외부 JOIN을 활용해야 한다
# 외부 조인
2개의 테이블을 조인할 때 필요한 내용이 한쪽 테이블에만 있어도 결과 추출 가능
자주 사용되는 방식은 아니지만 필요한 경우도 존재하기 때문에 알아두기
-- 외부 조인 기본 형식
select
열 이름1,
열 이름2,
...
from 첫번재 테이블(LEFT 테이블)
<LEFT / RIGHT / FULL> OUTER JOIN 두번째 테이블(RIGHT 테이블)
on 조인할 조건
[where 검색 조건];
내부 조인으로 해결하지 못한 '전체 회원의 구매기록(구매하지 않은 회원들도 포함하는 경우)' 확인
-- LEFT OUTER JOIN 수행
select
m.mem_id,
m.mem_name,
b.prod_name,
m.addr
from member as m -- LEFT OUTER JOIN이므로 member가 기준 테이블
left outer join buy as b on m.mem_id = b.mem_id -- OUTER 생략 가능(left join)
order by m.mem_id;
-- RIGHT OUTER JOIN 수행
select
m.mem_id,
m.mem_name,
b.prod_name,
m.addr
from member as m
right outer join buy as b on m.mem_id = b.mem_id -- buy가 기준 테이블
order by m.mem_id;


외부 조인 활용
회원 가입만 하고 한 번도 구매한 적이 없는 회원의 목록 추출
select distinct
m.mem_id,
b.prod_name,
m.mem_name,
m.addr
from member as m
left join buy as b on m.mem_id = b.mem_id
where b.prod_name is null -- 한 번도 구매를 진행하지 X = 물건 이름이 NULL인 경우
order by m.mem_id;

+
FULL OUTER JOIN
왼쪽 외부 조인과 오른쪽 외부 조인이 합쳐진 경우
왼쪽이든 오른쪽이든 한쪽에 들어 있는 내용은 모두 출력
(자주 사용되지는 않음)
# 기타 조인
상호 조인과 자체 조인 존재
상호 조인
한쪽 테이블의 모든 행이 다른 쪽 테이블의 모든 행을 조인시키는 기능
(전체 행의 개수 = 두 테이블의 각 행의 개수를 곱한 개수)
예) member 테이블과 buy테이블의 상호 조인 진행한다면

-- 상호 조인 수행
select
*
from buy
cross join member;

상호 조인의 특징
- ON 구문 사용할 수 없음
- 겨리과의 내요은 의미가 X(랜덤으로 조인 진행)
- 테스트를 하기 위한 대용향의 데이터 생성 시 활용
+
대용량 테이블 생성(CREATE TABLE ~ SELECT )
2개의 테이블의 크기가 커서 실제 테이블에 데이터 생성 시, 실습 시간이 오래 걸리므로
작은 테이블을 사용하여 새로운 테이블 생성 후 조회 진행
(데이터의 내용은 의미 없음)
# 자체 조인
자기 자신과 JOIN하는 방식
1개의 테이블을 사용하여 자체 조인 수행
-- 자체 조인 기본 형식
select
열 이름1,
열 이름2,
...
from 테이블명 별칭1
join 테이블명 별칭2
on 조인될 조건
[where 검색 조건];
emp_table을 생성하여 경리 부장의 상관의 연락처를 확인
create table emp_table(emp char(4), manager char(4), phone varchar(8));
insert into emp_table values('대표', null, '0000');
insert into emp_table values('영업이사', '대표', '1111');
insert into emp_table values('관리이사', '대표', '2222');
insert into emp_table values('정보이사', '대표', '3333');
insert into emp_table values('영업과장', '영업이사', '1111-1');
insert into emp_table values('경리부장', '관리이사', '2222-1');
insert into emp_table values('인사부장', '관리이사', '2222-2');
insert into emp_table values('개발팀장', '정보이사', '3333-1');
insert into emp_table values('개발주임', '정보이사', '3333-1-1');
select * from emp_table;
-- 경리부장의 직속 상관 연락처 조회
select
a.emp as 직원,
b.emp as 직속상관,
b.phone as '직속상관의 연락처'
from emp_table as a
join emp_table as b on a.manager = b.emp
where a.emp = '경리부장';

+
[195p 4번 문제 풀이]
회원으로 가입만 하고, 한번도 구매한 적이 없는 회원의 목록입니다.
빈칸에 들어갈 가장 적절한 것을 고르시오

➀ JOIN B.prod_name IS NULL② LIMIT B.prod_name IS NULL
➂ HAVING B.prod_name IS NULL
➃ WHERE B.prod_name IS NULL
✔️ SQL 프로그래밍
스토어드 프로시저는 MySQL의 프로그래밍 기능이 필요한 경우 사용하는 DB개체
(SQL 프로그래밍은 스토어드 프로시저 안에 만들어야 합니다)
-- 스토어드 프로시저의 기본 형식
DELIMITER $$
CREATE PROCEDURE -- 스토어드 프로시저의 이름()
BEGIN
-- 이 부분에 SQL 프로그래밍 코딩
END $$ -- 스토어드 프로시저 종료
DELIMITER ; -- 종료 문자를 한 칸 띄우고 세미클론(;)
-- 생성된 스토어드 프로시저 실행
CALL 스토어드 프로시저의 이름();
IF문
조건식이 참이라면 SQL문장들을 실행, 아닌 경우 그냥 넘어가는 기능 수행
IF <조건식> THEN
SQL 문장들
END IF;
SQL 문장들이 한 문장이라면 그 문장 사용해도 무방,
두 문장 이상 처리 시, BEGIN~ END로 묶어줘야 함
(습관적으로 사용하는 것이 용이)
-- 만약 ifProc1이 있다면 삭제
drop procedure if exists ifProc1;
-- $$ 사용하여 스토어드 프로시저의 끝을 나타냄(세미콜론(;)과 구분해서 사용)
delimiter $$
create procedure ifProc1() -- 스토어드 프로시저의 이름 지정
begin
if 100 = 100 then
select '100은 100과 같습니다'; -- 조건이 참인 경우, SQL 수행
end if;
end $$
delimiter ;
-- 생성된 ifProc1 스토어드 프로시저 수행
call ifProc1;

+
IF ~ ELSE
drop procedure if exists ifProc2;
delimiter $$
create procedure ifProc2()
begin
declare myNum int; -- declare 예약어 사용하여 myNum 변수 선언(데이터 형식 = int)
set myNum = 200; -- set 예약어 사용하여 myNum 변수에 200을 대입
if myNum = 100 then
select '100입니다.';
else
select '100이 아닙니다';
end if; -- if문 활용하여 100인지 아닌지 구분
end $$
delimiter ;
call ifProc2();

기존의 테이블과 IF문 활용
아이디가 APN인 회원의 데뷔 일자가 5년이 넘었는지 확인하고 넘었다면 축하 메세지를 출력
drop procedure if exists ifProc3;
delimiter $$
create procedure ifProc3()
begin
declare debutDate date; -- 데뷔일자
declare curDate date; -- 오늘 날짜
declare days int; -- 활동 일수
select debut_date into debutDate -- APN의 데뷔일자를 추출하여 debutDate에 저장
from market_db.member
where mem_id = 'APN';
set curDate = current_date(); -- 현재 날짜 저장
set days = datediff(curdate, debutDate); -- 데뷔 일자로부터 현재 날짜까지의 일수 days에 저장
if (days / 365) >= 5 then
select concat('데뷔한 지 ', days, '일이나 지났습니다. 축하합니다!');
else
select '데뷔한 지 ' + days + '일 밖에 안되었네요. 화이팅!';
end if; -- days에 저장된 일자를 365로 나누어 년으로 변환 후 5년 이상인지 아닌지 확인
end $$
delimiter ;
call ifProc3();
-- 날짜 및 시간 저장 : CURRENT_TIMESTAMP()

CASE문
여러 조건에 맞는 경우 각 조건에 맞는 데이터 대입
(다중 분기)
-- CASE ~ WHEN문의 기본 형식
CASE
WHEN 조건1 THEN SQL문장들1
WHEN 조건2 THEN SQL문장들2
WHEN 조건3 THEN SQL문장들3
ELSE SQL문장들4
END CASE;
시험 점수에 따른 학점
90점 이상 : A
80점 이상 : B
70점 이상 : C
60점 이상 : D
60점 미만 F
drop procedure if exists caseProc;
delimiter $$
create procedure caseProc()
begin
declare point int;
declare credit char(1);
set point = 88;
case
when point >= 90 then
set credit = 'A';
when point >= 80 then
set credit = 'B';
when point >= 70 then
set credit = 'C';
when point >= 60 then
set credit = 'D';
else set credit = 'F';
end case;
select concat('취득 점수 ==> ', point), concat('학점 ==> ', credit);
end $$
delimiter ;
call caseProc();
+
CASE문 활용
회원들의 총 구매액을 기준으로 회원 등급을 나눈다
-- 총 구매액이 많은 순서대로 정렬
select
mem_id, -- 1
sum(price * amount) as '총 구매액' -- 2
from buy
group by mem_id
order by 2 desc;
-- 회원 이름 추가
select
b.mem_id,
m.mem_name,
sum(b.price * b.amount) as '총 구매액'
from buy as b
join member as m on b.mem_id = m.mem_id
group by b.mem_id
order by 2 desc;

나머지 회원들에 대한 정보를 확인할 수 없으므로
right join을 진행하여 전체 회원 정보 조회하고
이를 바탕으로 CASE문 추가하여 회원 등급 나눔
select
m.mem_id,
m.mem_name,
sum(b.price * b.amount) as '총 구매액'
from buy as b
right join member as m on b.mem_id = m.mem_id
group by m.mem_id
order by 3 desc;
-- 회원 등급 구분(CASE절 활용)
select
m.mem_id,
m.mem_name,
sum(b.price * b.amount) as '총 구매액',
case
when (sum(b.price * b.amount) >= 1500) then '최우수고객'
when (sum(b.price * b.amount) >= 1000) then '우수고객'
when (sum(b.price * b.amount) >= 1) then '일반고객'
else '유령고객'
end '회원등급'
from buy as b
right join member as m on b.mem_id = m.mem_id
group by m.mem_id
order by 3 desc;


WHILE문
조건식이 참인 동안에 SQL문장들 계속 반복하는 기능 수행
WHILE <조건식> DO
SQL 문장들
END WHILE;
1부터 100까지 값을 모두 더하는 기능 수행
drop procedure if exists whileProc;
delimiter $$
create procedure whileProc()
begin
declare i int; -- 1에서 100까지 증가할 변수
declare hap int; -- 더한 값 누적할 변수
set i = 1;
set hap = 0;
while (i <= 100 ) do
set hap = hap + i; -- hap의 원래 값에 i를 더해서 다시 hap에 대입
set i = i + 1; -- i의 원래 값에 1을 더해서 다시 i에 대입
end while;
select '1부터 100까지의 합 ==>', hap;
end $$
delimiter ;
call whileProc();

ITERATE[레이블] : 지정한 레이블로 가서 계속 진행
LEAVE[레이블] : 지정한 레이블을 빠져나갑니다. 즉 while문 종료
1부터 100까지의 합(4의 배수 제외), 1000 넘으면 종료
drop procedure if exists whileProc2;
delimiter $$
create procedure whileProc2()
begin
declare i int;
declare hap int;
set i = 1;
set hap = 0;
mywhile: -- 레이블 지정
while (i <= 100 ) do
if (i%4 = 0) then
set i = i+1;
iterate mywhile; -- 지정한 label 문으로 가서 계속 진행
end if;
set hap = hap + i;
if (hap > 1000) then
leave mywhile; -- 지정한 lavel문을 떠남. while문 종료
end if;
set i = i + 1;
end while;
select '1부터 100까지의 합(4의 배수 제외), 1000 넘으면 종료 ==>', hap;
end $$
delimiter ;
call whileProc2();

동적 SQL
PREPARE : SQL문을 실행하지는 않고 미리 준비만 해놓음
EXECUTE : 준비한 SQL문 실행
DEALLOCATE PREPARE : 실행 후 문장을 해제하는데 사용
PREPARE문에서는 ?로 향후에 입력될 값을 비워 놓고,
EXECUTE에서 USING으로 ? 값을 전달 가능
예) 보안 중요 출입문에서 출입한 내역을 테이블에 기록
(태그한 순간의 시간과 날짜가 INSERT문으로 만들어져 입력되도록 수행)
drop table if exists gate_table;
create table gate_table (
id int auto_increment primary key, -- 아이디 자동으로 증가
entry_time datetime -- 출입하는 날짜와 시간 저장
);
set @curDate = current_timestamp(); -- 현재 날짜와 시간(변수)
prepare myQuery from 'insert into gate_table values(null, ?)'; -- ?값을 비워놓음
execute myQuery using @curDate; -- ?값에 @curDate값을 대입
deallocate prepare myQuery; -- prepare문장 해제
select * from gate_table;

이상으로 3주차 내용 정리를 마치겠습니다
혹시 위와 같은 내용을 더 자세히 공부해보고 싶다면
https://www.youtube.com/watch?v=1YmWy-7-OhQ
9~ 11강의 내용을 다루고 있으니 필요하신 분은 유튜브 영상을 참고하여 공부하시기 바랍니다.
'SQL > 혼자 공부하는 SQL' 카테고리의 다른 글
| [혼공S] 5주차_인덱스(Index) 원리와 MySQL 워크벤치 실습 (0) | 2025.02.12 |
|---|---|
| [혼공S] 4주차_제약조건을 활용한 테이블 및 가상 테이블(VIEW) 생성 및 활용 (0) | 2025.02.03 |
| [혼공S] 2주차_SELECT문과 WHERE절 활용 (0) | 2025.01.18 |
| [혼공S] 2주차 _SELECT문의 ORDER BY와 GROUP BY 활용 (0) | 2025.01.18 |
| [혼공S] 2주차_테이블의 데이터 입력, 수정, 삭제까지 (0) | 2025.01.18 |