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);