로그인
AUTH-LOGIN · /login
기능
7
API 호출
1
가드레일
11
id 가 테스트 이름
구현 단계
16 / 16
개발 스펙
이 화면에서 할 수 있어야 하는 것 (3)
- 아이디 · 비밀번호로 로그인한다.
- 카카오 로그인도 쓸 수 있다.
- 아이디/비밀번호 찾기로 이동한다.
비즈니스 규칙 (3)
- 비밀번호는 bcrypt 로 검증한다.근거: src/lib/auth.ts — 이관 시 bcrypt 로 옮겨 둔 상태.
- 수강 자격 플래그(prepare_yn · pay_yn · cyber_yn 등)를 세션에 함께 싣는다.근거: _Env.php 가 매 요청 계산하던 값.
- pw 가 빈 회원이 있다(소셜 전용으로 추정). 이 계정은 비밀번호 로그인이 불가능하다.
API (3)
| 메서드 | 경로 | 권한 | 모듈 · 설명 |
|---|---|---|---|
POST | /api/identity/login | 공개 | 회원 로그인. 성공 시 액세스 토큰 발급. domains:identity:identity-web요청: { loginId, password } 응답: { accessToken, refreshToken, member: { id, name, entitlements } } 401 INVALID_CREDENTIALS |
POST | /api/identity/token/refresh | 공개 | 토큰 재발급. domains:identity:identity-web |
GET | /api/identity/me | 수강생 | 내 정보와 수강 자격 플래그. domains:identity:identity-web |
가드레일 (11) — id 가 곧 테스트 이름이다
| id | 반드시 만족해야 하는 것 | 등급 |
|---|---|---|
signin-log-long-login-id | 아이디가 길어도 로그인이 되어야 한다. member_sign_log.member_id 는 varchar(20) 인데 member.login_id 는 50자까지 받는다. 자르지 않고 넣으면 INSERT 가 실패하고, REQUIRES_NEW 안에서 그 예외를 잡으면 rollback-only 로 커밋이 터져 로그인 실패로 번진다 — 실제로 그렇게 깨졌다. | critical |
signin-log-recorded | 로그인·로그아웃 이력이 원본 문구("로그인"/"로그아웃")로 남아야 한다. 13만 건이 2021년부터 쌓여 있다 — 값이 달라지면 한 줄로 이어 읽히지 않는다. | normal |
signin-log-does-not-break-login | 이력을 못 남겨도 로그인은 되어야 한다. 원본도 실패를 삼켰다. 기록 한 건보다 로그인이 되는 게 중요하다. | high |
signin-log-no-record-on-failure | 실패한 로그인은 이력에 남기지 않고, 통과시키지도 않아야 한다. 원본도 성공만 tMemberSignLog 에 넣었다. | high |
signin-log-logout-always-ok | 로그아웃은 아이디가 없거나 이상해도 500 을 내면 안 된다. 화면은 응답과 무관하게 로그아웃을 진행한다. | normal |
login-success-not-reported-as-failure | 로그인에 성공하면 실패 메시지가 뜨면 안 된다. signIn 에 redirectTo 를 주면 성공 시 NEXT_REDIRECT 를 던지는데, catch 가 그것까지 삼켜 성공을 실패로 뒤집은 적이 있다. | critical |
login-works-in-production-build | 프로덕션 빌드에서도 로그인과 세션 조회가 되어야 한다. Auth.js 가 Host 를 신뢰하지 않아 프로덕션에서만 UntrustedHost 로 전부 실패한 적이 있다. dev 에서는 드러나지 않는다. | critical |
jwt-secret-shared-between-servers | 인증 서버가 발급한 토큰을 API 서버가 검증할 수 있어야 한다. 설정이 없을 때 프로세스마다 랜덤 임시 키를 만들어, 로그인 후 모든 요청이 401 이 된 적이 있다. | critical |
login-no-user-enumeration | 없는 아이디와 틀린 비밀번호의 응답이 같아야 한다. | high |
login-empty-password-account-blocked | pw 가 빈 계정은 어떤 비밀번호로도 로그인되면 안 된다. bcrypt.compare 가 빈 해시에서 예외를 던지지 않도록 방어해야 한다. | critical |
login-token-has-no-secret | 토큰 페이로드에 비밀번호 해시 등 민감 정보를 담지 않는다. | critical |
구현 계획 (16/16)
- ●SignLogRepository 포트 (SignType 이 원본 한글 문구를 들고 있다)
identity-domain - ●별도 트랜잭션으로 기록하고 실패를 삼키는 어댑터
identity-infrastructure - ●LoginUseCase.login(ip) · logout(ip)
identity-app - ●POST /identity/logout — 토큰 방식이라 기록만 한다
identity-web - ●세션에 loginId 를 실어 로그아웃 때 넘긴다
wvctesol - ●도메인 모델과 포트 정의
identity-domain - ●LoginUseCase — 계정 존재 여부 은닉 · 빈 비밀번호 차단
identity-app - ●MemberJpaEntity · MemberRepository 구현 · bcrypt 인코더
identity-infrastructure - ●/api/identity/login 구현
identity-web - ●/api/identity/token/refresh 구현
identity-web - ●/api/identity/me 구현
identity-web - ●Next.js 를 BFF 로 전환 — Prisma 직접 접근 제거
wvctesol - ●BFF 라우트 /api/identity/auth/find-pw — 이게 없어 화면이 오래 망가져 있었다
wvctesol - ●BFF 라우트 /api/identity/auth/reset-password (확정)
wvctesol - ●재설정 화면 /find-pw/reset — 메일 링크가 가리키는 곳
wvctesol - ●재설정 토큰 키를 JwtTokenProvider 와 같은 고정 개발 키로 — 서버 둘이 나눠 실려 랜덤 키면 깨진다
identity-security
서버 사이드 구현 계획 — 자바 객체별 요구사항 (11/11)
| 객체 | 모듈 | 책임 · 지켜야 할 것 |
|---|---|---|
● Memberrecord | identity-domain | 인증과 자격 판단에 필요한 회원 정보를 담는다.
|
● Entitlementsrecord | identity-domain | 과정별 수강 자격. _Env.php:426~450 이 매 요청 계산하던 값.
|
● MemberRepositoryinterface | identity-domain | 회원 조회 포트.
|
● PasswordEncoderinterface | identity-domain | 비밀번호 검증·해싱 포트.
|
● LoginUseCaseservice | identity-app | 로그인 판단. 원본 member/login_proc.php.
|
● MemberJpaEntityentity | identity-infrastructure | member 테이블 매핑.
|
● MemberRepositoryAdapterclass | identity-infrastructure | 포트 구현. 레거시 표현을 도메인 타입으로 옮기는 유일한 지점.
|
● BcryptPasswordEncoderAdapterclass | identity-infrastructure | 회원 비밀번호 bcrypt 검증.
|
● JwtTokenProviderclass | identity-security | 토큰 발급·검증.
|
● IdentityControllercontroller | identity-web | POST /api/identity/login.
|
● MemberResponserecord | identity-web | 회원 응답 DTO.
|
테스트 방법 (11)
| 가드레일 | 종류 | 확인 방법 |
|---|---|---|
signin-log-long-login-id | e2e | 20자 넘는 아이디로 가입 → 로그인·로그아웃 성공 |
signin-log-recorded | unit | 로그인·로그아웃 후 "로그인:아이디" · "로그아웃:아이디" 가 순서대로 기록된다 |
signin-log-does-not-break-login | e2e | 로그인이 토큰을 돌려준다 |
signin-log-no-record-on-failure | e2e | 틀린 비밀번호 → INVALID_CREDENTIALS |
signin-log-logout-always-ok | e2e | 아이디 없음·빈 본문에도 500 이 아니다 |
login-success-not-reported-as-failure | e2e | 올바른 계정으로 로그인 → 오류 문구 없이 로그인 화면을 벗어난다 |
login-works-in-production-build | e2e | next build && next start 로 띄운 뒤 로그인 → 성공, 로그에 UntrustedHost 가 없다 |
jwt-secret-shared-between-servers | integration | auth(:8081) 로그인 토큰으로 api(:8080) 의 /api/learning/students/me 호출 → 200 |
login-no-user-enumeration | e2e | 없는 아이디와 틀린 비밀번호 두 요청의 status·body·응답시간대가 동일 |
login-empty-password-account-blocked | unit | pw 가 빈 회원으로 임의 비밀번호 로그인 → 401, 예외 없이 처리 |
login-token-has-no-secret | e2e | 발급 토큰 payload 를 디코드해 pw/hash 계열 키가 없음을 단언 |
스냅샷

기능 목록 (7)
데이터
- ·API 조회
/api/identity/auth/find-id
입력
- ·폼
1개 - ·제출 버튼
1개 - ·입력 필드
2개
액션
- ·서버 액션
login
인증
- ·세션 확인
- ·로그인/로그아웃 호출
화면 이동
← 이 화면으로 오는 곳
AUTH-FIND-ID/find-idAUTH-FIND-PW-RESET/find-pw/resetAUTH-JOIN/joinCLASS-ONLINE/my-class/onlineCLASS-RESULT-OF-COURSE/my-class/result-of-courseCLASS-STUDENT-INFORMATION/my-class/student-informationCLASS-TEC-ONLINE/my-class/tec-onlineCLASS-TEC-RESULT-OF-COURSE/my-class/tec-result-of-courseCLASS-TEC-STUDENT-INFORMATION/my-class/tec-student-informationCLASS-TEC-UNIT-QUIZ-DETAIL/my-class/tec-unit-quiz/[unit]/[kind]CLASS-TEC-UNIT-RESULT-DETAIL/my-class/tec-unit-result/[unit]/[kind]CLASS-UNIT-QUIZ-DETAIL/my-class/unit-quiz/[unit]/[kind]CLASS-UNIT-RESULT-DETAIL/my-class/unit-result/[unit]/[kind]QNA-QA-LIST-WRITE/qna/qa-list/writeGNB
이 화면에서 가는 곳 →
AUTH-FIND-ID/find-idAUTH-FIND-PW/find-pwAUTH-JOIN/joinHOME/
연결
API
/api/identity/auth/find-id
소스
src/app/login/page.tsx