diff --git a/backend/pom.xml b/backend/pom.xml
index 9cac2e2..a174705 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -38,6 +38,17 @@
org.springframework.boot
spring-boot-starter-data-jpa
+
+
+ org.flywaydb
+ flyway-core
+
+
+ org.flywaydb
+ flyway-database-postgresql
+
org.springframework.boot
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
index 9fde9ad..0615d2f 100644
--- a/backend/src/main/resources/application.yml
+++ b/backend/src/main/resources/application.yml
@@ -10,20 +10,27 @@ spring:
password: ${DB_PASSWORD:mirim123}
jpa:
hibernate:
- # 학습 포인트: ddl-auto: update의 의미 —
- # 앱이 뜰 때 Hibernate가 엔티티(@Entity 클래스)와 실제 테이블을 비교해서
- # 없는 테이블/컬럼을 자동으로 만들어 준다(CREATE/ALTER를 대신 실행).
- # 개발 단계에선 편하지만 운영에선 위험하다:
- # - 컬럼 삭제/이름 변경은 반영하지 못해 엔티티와 테이블이 조용히 어긋난다.
- # - 어떤 DDL이 언제 실행됐는지 기록이 남지 않아 되돌리기가 불가능하다.
- # 운영에선 validate(검증만)로 바꾸고 Flyway 같은 마이그레이션 도구로 스키마를 관리한다.
- ddl-auto: update # 개발 단계: 엔티티에 맞춰 테이블 자동 생성/수정
+ # 학습 포인트: 이제 스키마는 Flyway가 소유한다. Hibernate는 validate로 "엔티티와 실제
+ # 테이블이 일치하는지 검증"만 하고, 테이블을 만들거나 바꾸지 않는다.
+ # - update였을 때의 문제(컬럼 삭제 미반영·변경 이력 없음·조용한 드리프트)를 끝낸다.
+ # - 엔티티를 바꿨는데 마이그레이션(Vn.sql)을 안 쓰면 validate가 기동 시 실패해 알려 준다
+ # → "엔티티와 DB를 함께 바꿔라"를 기계가 강제한다.
+ ddl-auto: validate
# open-in-view: 화면 렌더링이 끝날 때까지 DB 커넥션을 물고 있는 기능. REST API 서버에선
# 커넥션 낭비일 뿐이라 끈다. (끄면 LAZY 로딩은 반드시 @Transactional 안에서 끝내야 한다)
open-in-view: false
properties:
hibernate:
format_sql: true
+ # Flyway — DB 스키마 마이그레이션(버전 관리). classpath의 db/migration/V*.sql을 순서대로 적용.
+ flyway:
+ # 학습 포인트: baseline-on-migrate —
+ # 이미 테이블이 있는(비어있지 않은) 기존 운영 DB엔 V1을 "실행"하지 않고 "이미 반영됨"으로
+ # 표시(baseline)만 한다. 안 그러면 V1의 CREATE TABLE이 "이미 존재"로 터진다.
+ # 빈 DB(신규/개발)에선 baseline이 일어나지 않고 V1부터 정상 실행돼 스키마를 만든다.
+ # → 같은 마이그레이션 파일로 "기존 운영 DB 편입"과 "신규 DB 생성"을 모두 안전하게 처리.
+ baseline-on-migrate: true
+ baseline-version: 1
server:
port: 8080
diff --git a/backend/src/main/resources/db/migration/V1__baseline.sql b/backend/src/main/resources/db/migration/V1__baseline.sql
new file mode 100644
index 0000000..88e061a
--- /dev/null
+++ b/backend/src/main/resources/db/migration/V1__baseline.sql
@@ -0,0 +1,370 @@
+-- V1__baseline.sql — 운영 DB의 "현재 스키마"를 그대로 박제한 베이스라인.
+--
+-- 이 파일이 하는 일:
+-- Flyway 도입 시점의 스키마 전체(테이블 13개·제약·인덱스)를 기록한다.
+-- 이 시점 이후의 모든 스키마 변경은 V2, V3... 로 "의도를 남기며" 추가한다.
+--
+-- 학습 포인트 - 왜 ddl-auto:update에서 Flyway로 옮기나?
+-- ddl-auto:update는 앱이 뜰 때 엔티티에 맞춰 테이블을 자동 변경했다. 편했지만:
+-- - 컬럼 삭제/이름변경을 반영 못 해 엔티티와 DB가 조용히 어긋나고,
+-- - 무엇이 언제 바뀌었는지 기록이 없어 되돌릴 수 없었다.
+-- - 그 흔적이 이 스키마에도 남아 있다(예: submissions에 같은 컬럼 unique 제약이
+-- 두 개 - 하나는 명시명, 하나는 Hibernate 자동해시명 uk...). 이런 드리프트를
+-- 끝내려고 Flyway로 옮긴다.
+-- 이제부터 스키마는 "버전 관리되는 마이그레이션"으로만 바뀐다. Hibernate는 ddl-auto:validate로
+-- "엔티티와 DB가 맞는지 검증"만 한다(스키마를 건드리지 않는다).
+--
+-- 주의: 이 베이스라인은 자동 캡처된 현재 상태라 제약/시퀀스 이름이 Hibernate 자동생성(해시)이다.
+-- 깔끔한 재명명은 이후 별도 마이그레이션(Vn)에서 다룬다 - 베이스라인은 "있는 그대로"가 원칙.
+
+CREATE TABLE public.assignments (
+ id bigint NOT NULL,
+ day integer NOT NULL,
+ kind character varying(255) NOT NULL,
+ summary text,
+ title character varying(255) NOT NULL,
+ week integer NOT NULL
+);
+
+ALTER TABLE public.assignments ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
+ SEQUENCE NAME public.assignments_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1
+);
+
+CREATE TABLE public.audit_log (
+ id bigint NOT NULL,
+ action character varying(30) NOT NULL,
+ actor character varying(40) NOT NULL,
+ actor_role character varying(20) NOT NULL,
+ created_at timestamp(6) with time zone NOT NULL,
+ detail character varying(200),
+ ip character varying(45) NOT NULL,
+ target_user_id bigint,
+ CONSTRAINT audit_log_action_check CHECK (((action)::text = ANY ((ARRAY['EXPORT_STUDENT_CSV'::character varying, 'VIEW_STUDENT_ROSTER'::character varying, 'RESET_STUDENT_PASSWORD'::character varying, 'PURGE_STUDENT'::character varying])::text[])))
+);
+
+ALTER TABLE public.audit_log ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
+ SEQUENCE NAME public.audit_log_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1
+);
+
+CREATE TABLE public.checklist_items (
+ id bigint NOT NULL,
+ day integer,
+ label character varying(255) NOT NULL,
+ sort_order integer NOT NULL,
+ week integer NOT NULL
+);
+
+ALTER TABLE public.checklist_items ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
+ SEQUENCE NAME public.checklist_items_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1
+);
+
+CREATE TABLE public.coding_problem (
+ id bigint NOT NULL,
+ description text NOT NULL,
+ difficulty character varying(8) NOT NULL,
+ function_name character varying(60),
+ language character varying(8) NOT NULL,
+ sort_order integer NOT NULL,
+ starter_code text NOT NULL,
+ tests_json text,
+ title character varying(255) NOT NULL,
+ topic character varying(60) NOT NULL,
+ setup_sql text
+);
+
+ALTER TABLE public.coding_problem ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
+ SEQUENCE NAME public.coding_problem_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1
+);
+
+CREATE TABLE public.coding_submission (
+ id bigint NOT NULL,
+ attempts integer NOT NULL,
+ code text NOT NULL,
+ feedback text,
+ passed_count integer,
+ results_json text,
+ status character varying(12) NOT NULL,
+ submitted_at timestamp(6) with time zone NOT NULL,
+ total_count integer,
+ problem_id bigint NOT NULL,
+ user_id bigint NOT NULL
+);
+
+ALTER TABLE public.coding_submission ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
+ SEQUENCE NAME public.coding_submission_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1
+);
+
+CREATE TABLE public.course_progress (
+ id bigint NOT NULL,
+ completed_at timestamp(6) with time zone NOT NULL,
+ course_slug character varying(60) NOT NULL,
+ user_id bigint NOT NULL
+);
+
+ALTER TABLE public.course_progress ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
+ SEQUENCE NAME public.course_progress_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1
+);
+
+CREATE TABLE public.documents (
+ id bigint NOT NULL,
+ audience character varying(255) NOT NULL,
+ category character varying(255) NOT NULL,
+ file_path character varying(255) NOT NULL,
+ slug character varying(255) NOT NULL,
+ sort_order integer NOT NULL,
+ title character varying(255) NOT NULL,
+ week integer
+);
+
+ALTER TABLE public.documents ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
+ SEQUENCE NAME public.documents_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1
+);
+
+CREATE TABLE public.progress (
+ id bigint NOT NULL,
+ checked_at timestamp(6) with time zone NOT NULL,
+ item_id bigint NOT NULL,
+ user_id bigint NOT NULL
+);
+
+ALTER TABLE public.progress ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
+ SEQUENCE NAME public.progress_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1
+);
+
+CREATE TABLE public.quiz_question (
+ id bigint NOT NULL,
+ answer_index integer NOT NULL,
+ course_slug character varying(60) NOT NULL,
+ explanation text NOT NULL,
+ option1 text NOT NULL,
+ option2 text NOT NULL,
+ option3 text NOT NULL,
+ option4 text NOT NULL,
+ question text NOT NULL,
+ sort_order integer NOT NULL
+);
+
+ALTER TABLE public.quiz_question ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
+ SEQUENCE NAME public.quiz_question_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1
+);
+
+CREATE TABLE public.quiz_result (
+ id bigint NOT NULL,
+ course_slug character varying(60) NOT NULL,
+ score integer NOT NULL,
+ submitted_at timestamp(6) with time zone NOT NULL,
+ total integer NOT NULL,
+ user_id bigint NOT NULL
+);
+
+ALTER TABLE public.quiz_result ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
+ SEQUENCE NAME public.quiz_result_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1
+);
+
+CREATE TABLE public.student_profile (
+ id bigint NOT NULL,
+ address text NOT NULL,
+ birth_date date NOT NULL,
+ company_email_local character varying(40) NOT NULL,
+ emergency_name character varying(30) NOT NULL,
+ emergency_phone character varying(20) NOT NULL,
+ emergency_relation character varying(20) NOT NULL,
+ major character varying(60) NOT NULL,
+ phone character varying(20) NOT NULL,
+ school character varying(60) NOT NULL,
+ user_id bigint NOT NULL,
+ privacy_consent_at timestamp(6) with time zone NOT NULL
+);
+
+ALTER TABLE public.student_profile ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
+ SEQUENCE NAME public.student_profile_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1
+);
+
+CREATE TABLE public.submissions (
+ id bigint NOT NULL,
+ content text NOT NULL,
+ feedback text,
+ link character varying(255),
+ status character varying(255) NOT NULL,
+ submitted_at timestamp(6) with time zone NOT NULL,
+ assignment_id bigint NOT NULL,
+ user_id bigint NOT NULL
+);
+
+ALTER TABLE public.submissions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
+ SEQUENCE NAME public.submissions_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1
+);
+
+CREATE TABLE public.users (
+ id bigint NOT NULL,
+ created_at timestamp(6) with time zone NOT NULL,
+ name character varying(255) NOT NULL,
+ password_hash character varying(255) NOT NULL,
+ role character varying(255) NOT NULL,
+ track character varying(255) NOT NULL,
+ username character varying(255) NOT NULL
+);
+
+ALTER TABLE public.users ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
+ SEQUENCE NAME public.users_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1
+);
+
+ALTER TABLE ONLY public.assignments
+ ADD CONSTRAINT assignments_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY public.audit_log
+ ADD CONSTRAINT audit_log_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY public.checklist_items
+ ADD CONSTRAINT checklist_items_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY public.coding_problem
+ ADD CONSTRAINT coding_problem_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY public.coding_submission
+ ADD CONSTRAINT coding_submission_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY public.course_progress
+ ADD CONSTRAINT course_progress_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY public.documents
+ ADD CONSTRAINT documents_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY public.progress
+ ADD CONSTRAINT progress_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY public.quiz_question
+ ADD CONSTRAINT quiz_question_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY public.quiz_result
+ ADD CONSTRAINT quiz_result_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY public.student_profile
+ ADD CONSTRAINT student_profile_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY public.submissions
+ ADD CONSTRAINT submissions_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY public.progress
+ ADD CONSTRAINT uk3328ayw5asr3mnmp0c31n21jf UNIQUE (user_id, item_id);
+
+ALTER TABLE ONLY public.student_profile
+ ADD CONSTRAINT uk3whrp0g2trsa8qsb2nk142jea UNIQUE (company_email_local);
+
+ALTER TABLE ONLY public.student_profile
+ ADD CONSTRAINT uk99mm2qc8gq78mojsjmdhqqrtd UNIQUE (user_id);
+
+ALTER TABLE ONLY public.submissions
+ ADD CONSTRAINT uk_submissions_assignment_user UNIQUE (assignment_id, user_id);
+
+ALTER TABLE ONLY public.coding_submission
+ ADD CONSTRAINT ukepcri27qov9okseajuck21p1q UNIQUE (problem_id, user_id);
+
+ALTER TABLE ONLY public.course_progress
+ ADD CONSTRAINT ukk2refcthyrufe59nnxsjkxklv UNIQUE (user_id, course_slug);
+
+ALTER TABLE ONLY public.submissions
+ ADD CONSTRAINT ukmg9jk4whp661fe4kyo8pdapkf UNIQUE (assignment_id, user_id);
+
+ALTER TABLE ONLY public.documents
+ ADD CONSTRAINT ukpbof73ceduubbvfrqc0wj4eb1 UNIQUE (slug);
+
+ALTER TABLE ONLY public.users
+ ADD CONSTRAINT ukr43af9ap4edm43mmtq01oddj6 UNIQUE (username);
+
+ALTER TABLE ONLY public.users
+ ADD CONSTRAINT users_pkey PRIMARY KEY (id);
+
+CREATE INDEX ix_audit_log_created_at ON public.audit_log USING btree (created_at);
+
+ALTER TABLE ONLY public.progress
+ ADD CONSTRAINT fk4gq8i0p0nmn2dwgcq11nmpjm4 FOREIGN KEY (item_id) REFERENCES public.checklist_items(id);
+
+ALTER TABLE ONLY public.submissions
+ ADD CONSTRAINT fk760bgu69957phd7hax608jdms FOREIGN KEY (user_id) REFERENCES public.users(id);
+
+ALTER TABLE ONLY public.coding_submission
+ ADD CONSTRAINT fk78tg75ufvjeo1lc6g7jc8y9g0 FOREIGN KEY (problem_id) REFERENCES public.coding_problem(id);
+
+ALTER TABLE ONLY public.progress
+ ADD CONSTRAINT fk7fyumbty8qgbd7sfbbjnqdo62 FOREIGN KEY (user_id) REFERENCES public.users(id);
+
+ALTER TABLE ONLY public.student_profile
+ ADD CONSTRAINT fkh6555c9k0gv0yddac6llslk3t FOREIGN KEY (user_id) REFERENCES public.users(id);
+
+ALTER TABLE ONLY public.coding_submission
+ ADD CONSTRAINT fkiym0dwju9tunrvp1mjdep8bl4 FOREIGN KEY (user_id) REFERENCES public.users(id);
+
+ALTER TABLE ONLY public.course_progress
+ ADD CONSTRAINT fkmootcccgd4bqe8cjtma7stkp1 FOREIGN KEY (user_id) REFERENCES public.users(id);
+
+ALTER TABLE ONLY public.quiz_result
+ ADD CONSTRAINT fknuben697e3lorbhgsbif22f2t FOREIGN KEY (user_id) REFERENCES public.users(id);
+
+ALTER TABLE ONLY public.submissions
+ ADD CONSTRAINT fkrirbb44savy2g7nws0hoxs949 FOREIGN KEY (assignment_id) REFERENCES public.assignments(id);