서버 사이드

Spring Boot 멀티모듈 구성 · 개발 계정 · 전역 가드레일

실행 서버
2
도메인
8
× 4 계층
Gradle 모듈
43
전역 가드레일
21

개발 계정

로컬 DB(:5437)에만 있는 검증용 계정입니다. 운영에 만들지 마세요.

용도아이디비밀번호로그인 위치비고
관리자wvcadmin1111/admin/login결제 관리 · 입금확인 · 환불. admin 테이블, approval=Y. 비밀번호는 bcrypt 로 저장돼 있다.
수강생quiztester1111/loginmember.id=97610. 수강기간 2026-01-01~2026-12-31, TESOL·TEC 플래그 모두 Y 라 시험이 바로 열린다. TESOL/TEC 각 UNIT1·2 응시 기록이 들어 있다(UNIT1 통과, UNIT2 는 A 불합격 후 B 통과).

wvctesol-auth :8081

로그인 · 토큰 발급 · 회원 정보

servers:wvctesol-auth

인증 · 회원

wvctesol-api :8080

인증을 제외한 도메인 전체. 토큰은 검증만 한다.

servers:wvctesol-api

상품 카탈로그수강신청 · 결제수강 · 커리큘럼시험 · 평가커뮤니티마케팅 콘텐츠알림

계층 구조와 의존 방향

domain ← app ← infrastructure · web 단방향입니다. 역방향 의존이 생기면 설계가 무너집니다.

모듈역할의존규칙
-domain순수 비즈니스 규칙shared:common-domain 만스프링에 의존하지 않는다. 엔티티 · 값 객체 · 도메인 이벤트 · 포트(인터페이스)만 둔다.
-app유스케이스같은 도메인의 -domain, shared:common-app트랜잭션 경계를 여기서 잡는다. 포트를 호출할 뿐 구현은 모른다.
-infrastructure영속성 어댑터같은 도메인의 -domain · -appJPA 엔티티와 포트 구현체. 레거시 스키마의 표현(문자열 날짜, Y/N 플래그)을 여기서 변환한다.
-webREST API같은 도메인의 -domain · -app, shared:common-web요청 검증과 응답 변환만. 비즈니스 판단을 두지 않는다.

전역 가드레일 (21)

모든 화면·API 가 지켜야 합니다. id 는 E2E 테스트 이름으로 그대로 씁니다.

id규칙등급
consultation-submits-to-server
1:1 무료컨설팅 신청은 실제로 접수돼야 한다.
홈은 서버로 보내지 않고 "신청이 완료되었습니다" 알림만 띄웠고, TESOL · TEC 과정 소개는 입력칸이 readOnly 에 신청 버튼에 onClick 조차 없었다. 원본은 세 화면 모두 tesolLab/leveltest_form_v_01.php 로 보내 apply 에 접수한다.
critical
consultation-time-options
상담 가능 시간대가 원본과 같아야 한다 — 평일 10~22시, 주말 10~18시.
홈에는 평일 7개만 있고 주말이 없었다(index.php:1045~1066 의 generateTimeOptions).
normal
profile-change-targets-token-owner
이름 · 영문 이름은 토큰의 회원 것만 바꾼다.
원본은 화면이 보낸 아이디를 세션과 대조해 막았다(name_update.php:28). 우리는 요청에 회원을 담는 자리 자체가 없어 지목할 방법이 없다.
critical
profile-english-name-letters-only
영문 이름은 영문자로만 받는다.
자격증에 그대로 찍히고 발급 뒤에는 되돌릴 수 없다(원본 입학신청서 안내에도 적혀 있다). 원본은 값을 검사하지 않았다 — 한글이나 숫자가 섞이면 자격증이 잘못 나간다.
high
assets-served-locally
화면이 원본 사이트로 자산을 요청하지 않는다.
게시글 본문에 이미지 절대주소가 그대로 박혀 있었다. www 없는 주소는 인증서 이름이 맞지 않아 브라우저가 막고 http 는 그 쪽으로 리다이렉트돼, 원본 주소가 든 글 1,001건 중 989건이 이미 깨져 있었다. 이미지 3,446개(1.1GB)를 우리 디스크(ASSET_ROOT)로 옮기고 주소를 전부 상대경로로 바꿨다. 원본이 내려가도 화면은 그대로다.
critical
assets-no-broken-images
게시글 본문의 이미지가 실제로 그려져야 한다.
주소만 바꾸고 파일이 없으면 남의 서버에서 깨지던 것이 우리 서버에서 404 가 될 뿐이다.
high
ssr-matches-client
서버가 그린 화면과 브라우저가 그린 화면이 같아야 한다.
헤더가 로그인 상태를 클라이언트에서만 알아, 서버는 로그인한 사람에게도 "로그인 / 무료회원가입" 을 그리고 브라우저가 그것을 "마이클래스 / 로그아웃" 으로 갈아끼웠다. 원본(PHP)은 서버에서 맞는 헤더를 그렸다. React 는 이것을 불일치로 보고 화면을 통째로 다시 그렸다(#418). 세션을 서버에서 읽어 내려 주어 고쳤고, 그 대가로 모든 화면이 요청마다 그려진다 — 로그인 여부에 따라 달라지는 화면을 미리 구워 두면 안 되므로 그게 맞다.
critical
board-hit-only-on-read
게시판 목록을 여는 것만으로 글 상세를 불러오면 안 된다 — 조회수가 오른다.
Next 의 <Link> 는 화면에 보이는 링크의 대상을 미리 그려 둔다. 상세는 그리면서 조회수를 1 올리므로 목록을 열 때마다 그 화면의 글이 한꺼번에 읽힌 것으로 집계됐다(공지 · Q&A · 합격수기). 원본에는 미리 받아 오는 동작이 없었다.
high
global-no-price-from-client
금액은 절대 클라이언트가 보낸 값을 쓰지 않는다. 서버가 product 테이블에서 다시 읽어야 한다.
원본은 세션에 담아 대조했다(products/payment_proc.php:50~78).
critical
global-server-side-guard
화면 진입을 막는 검사는 제출(서버 액션 · API)에서도 똑같이 한 번 더 해야 한다. 화면 가드만 있으면 직접 POST 로 우회된다.
원본도 unitSloving.php 와 quiz_proc.php 에 같은 검사를 복붙해 두었다.
critical
global-ddl-auto-validate
JPA 는 스키마를 변경하지 않는다(ddl-auto: validate). 실데이터가 들어 있는 DB 다.
critical
global-no-secret-in-source
PG 상점키 · DB 비밀번호 등은 소스에 두지 않고 환경변수로만 받는다.
원본은 NICEPAY/nicepay_init_hiyou.php 에 상점키를 그대로 적어 두었다.
critical
global-domain-purity
-domain 모듈은 스프링·JPA 에 의존하지 않는다. 의존이 생기면 빌드로 막는다.
high
global-legacy-format-at-edge
레거시 표현(regdate 'YYYYMMDDHHmmss' varchar, Y/N char(1))은 -infrastructure 에서만 다룬다. 도메인 모델은 LocalDateTime · boolean 을 쓴다.
high
orphan-records-not-in-my-data
회원이 없는(탈퇴·삭제된) 기록이 내 조회에 섞이면 안 된다.
다섯 표에 1,181건 남아 있다 — certificate_post 656 · lecture_progress_total 164 · payment 154 · lecture_progress 116 · quiz_result 91. 지우는 것은 사용자 결정이라 섞이지 않는 것만 붙잡아 둔다.
high
orphan-records-admin-scoped
관리자 조회도 회원 하나씩만 봐야 한다. 없는 회원이면 빈 결과다.
high
orphan-records-member-list-clean
회원 수 집계는 member 표만 세야 한다. 고아 기록이 수를 부풀리면 안 된다.
normal
outline-titles-match-db
화면에 박아 둔 커리큘럼 제목이 DB 와 갈라지면 안 된다.
원본도 하드코딩이었다(courseOutline.php 는 SQL 0건). 그래서 서버로 옮기지 않았지만, 같은 내용이 DB 에도 있어 갈라지면 커리큘럼 화면과 강의실이 서로 다른 것을 보여 준다. 유닛1 하나는 원본에서도 갈라져 있어 기준선으로 둔다.
normal
outline-tec-titles-match-db
TEC 커리큘럼 제목도 DB 와 같아야 한다.
normal
hardcoded-screens-render
박아 둔 데이터로 그리는 화면들이 오류 없이 떠야 한다.
normal
global-error-response-shape
모든 API 오류는 같은 형태로 응답한다: { code, message, fieldErrors? }.
normal

가드레일 E2E 실행 방법

가드레일의 id 가 곧 테스트 이름입니다. 스펙에서 id 를 바꾸면 테스트도 같이 바꿔야 합니다.

# 서버 둘을 먼저 띄운다 (wvctesol-server)
./gradlew :servers:wvctesol-auth:bootRun &   # :8081
./gradlew :servers:wvctesol-api:bootRun &    # :8080

# 앱에서 (wvctesol)
pnpm test:e2e          # 전체 76건
pnpm test:e2e:api      # 브라우저 없이 API 규칙만 (빠름)
pnpm test:e2e:ui       # 실패를 눈으로 볼 때

로그인 관련 가드레일은 next build && next start 로 띄운 프로덕션 빌드에서도 한 번 돌려야 합니다 — Auth.js 의 Host 신뢰 문제처럼 dev 에서는 드러나지 않는 것이 있습니다.