diff --git a/src/main/java/com/sb/web/auth/domain/Member.java b/src/main/java/com/sb/web/auth/domain/Member.java index d193699..5a671c4 100644 --- a/src/main/java/com/sb/web/auth/domain/Member.java +++ b/src/main/java/com/sb/web/auth/domain/Member.java @@ -23,8 +23,9 @@ public class Member implements Serializable { private String password; // BCrypt 해시 (소셜 전용은 null) private String name; private String email; - private String provider; // LOCAL / NAVER + private String provider; // LOCAL / NAVER / GOOGLE private String providerId; // 소셜 제공자 측 고유 ID + private String googleId; // 구글 sub (계정 연결용 — LOCAL 계정에 구글을 연결해도 provider 는 유지) private String role; // USER / ADMIN private String status; // ACTIVE / SUSPENDED / WITHDRAWN private LocalDateTime createdAt; diff --git a/src/main/java/com/sb/web/auth/mapper/MemberMapper.java b/src/main/java/com/sb/web/auth/mapper/MemberMapper.java index df2550c..f1c72f2 100644 --- a/src/main/java/com/sb/web/auth/mapper/MemberMapper.java +++ b/src/main/java/com/sb/web/auth/mapper/MemberMapper.java @@ -22,6 +22,15 @@ public interface MemberMapper { Member findByProvider(@Param("provider") String provider, @Param("providerId") String providerId); + /** 구글 sub 로 회원 조회 (연결된 LOCAL 계정 + 순수 구글 계정 모두). */ + Member findByGoogleId(@Param("googleId") String googleId); + + /** 구글 로그인 시 연결 대상이 될 같은 이메일의 기존 계정(LOCAL 우선, 미연결만). */ + Member findByEmailForLink(@Param("email") String email); + + /** 기존 계정에 구글 sub 연결 (provider 유지). */ + int linkGoogle(@Param("id") Long id, @Param("googleId") String googleId); + int countByLoginId(@Param("loginId") String loginId); int insert(Member member); diff --git a/src/main/java/com/sb/web/auth/service/AuthService.java b/src/main/java/com/sb/web/auth/service/AuthService.java index cf3ef51..2bc7b6e 100644 --- a/src/main/java/com/sb/web/auth/service/AuthService.java +++ b/src/main/java/com/sb/web/auth/service/AuthService.java @@ -183,12 +183,30 @@ public class AuthService { } String sub = String.valueOf(info.get("sub")); String email = info.get("email") == null ? null : String.valueOf(info.get("email")); + boolean emailVerified = "true".equals(String.valueOf(info.get("email_verified"))); String name = info.get("name") != null ? String.valueOf(info.get("name")) : (email != null ? email.split("@")[0] : "사용자"); - Member member = memberMapper.findByProvider("GOOGLE", sub); + // 1) 구글 sub 로 이미 연결/가입된 계정 조회 + Member member = memberMapper.findByGoogleId(sub); + + // 2) 없으면 같은 (검증된)이메일의 기존 계정에 구글 연결 — 중복 계정/데이터 분리 방지 + if (member == null && email != null && !email.isBlank() && emailVerified) { + Member existing = memberMapper.findByEmailForLink(email); + if (existing != null) { + if (!"ACTIVE".equals(existing.getStatus())) { + throw new ApiException(HttpStatus.FORBIDDEN, "사용할 수 없는 계정입니다."); + } + memberMapper.linkGoogle(existing.getId(), sub); + existing.setGoogleId(sub); + member = existing; + log.info("[google] linked to existing member id={} provider={} email={}", + member.getId(), member.getProvider(), email); + } + } + + // 3) 그래도 없으면 신규 구글 계정 생성(최초 로그인 = 가입). 가입 제한 시 차단. if (member == null) { - // 최초 로그인 = 가입. 가입 제한 시 차단. if (!appSettingService.isSignupEnabled()) { throw new ApiException(HttpStatus.FORBIDDEN, "현재 회원가입이 제한되어 있습니다."); } @@ -197,6 +215,7 @@ public class AuthService { .email(email) .provider("GOOGLE") .providerId(sub) + .googleId(sub) .role("USER") .status("ACTIVE") .build(); diff --git a/src/main/resources/db/member.sql b/src/main/resources/db/member.sql index 6a32d76..56a9184 100644 --- a/src/main/resources/db/member.sql +++ b/src/main/resources/db/member.sql @@ -20,6 +20,13 @@ CREATE TABLE IF NOT EXISTS member ( UNIQUE KEY uk_member_provider (provider, provider_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- 구글 계정 연결: 기존 LOCAL 계정에 구글 sub 를 연결해 같은 이메일이면 한 계정으로 통합한다. +-- (provider 는 LOCAL 로 유지 → 아이디/비밀번호 로그인·프로필 편집 그대로) MariaDB IF NOT EXISTS 로 멱등. +ALTER TABLE member ADD COLUMN IF NOT EXISTS google_id VARCHAR(100) NULL COMMENT '구글 sub (계정 연결용)' AFTER provider_id; +ALTER TABLE member ADD UNIQUE KEY IF NOT EXISTS uk_member_google_id (google_id); +-- 기존 구글 계정(provider=GOOGLE)의 sub 를 google_id 로 백필(멱등 — 이미 채워졌으면 아무 것도 안 함). +UPDATE member SET google_id = provider_id WHERE provider = 'GOOGLE' AND google_id IS NULL AND provider_id IS NOT NULL; + -- ============================================================ -- 앱 전역 설정 (단일 행, id=1). 관리자 화면에서 변경. -- ============================================================ diff --git a/src/main/resources/mapper/MemberMapper.xml b/src/main/resources/mapper/MemberMapper.xml index 7ed95a1..fd7d92b 100644 --- a/src/main/resources/mapper/MemberMapper.xml +++ b/src/main/resources/mapper/MemberMapper.xml @@ -11,6 +11,7 @@ + @@ -18,7 +19,7 @@ - id, login_id, password, name, email, provider, provider_id, role, status, created_at, updated_at + id, login_id, password, name, email, provider, provider_id, google_id, role, status, created_at, updated_at + + + + + + + + + UPDATE member SET google_id = #{googleId}, updated_at = NOW() WHERE id = #{id} + + - INSERT INTO member (login_id, password, name, email, provider, provider_id, role, status, created_at, updated_at) + INSERT INTO member (login_id, password, name, email, provider, provider_id, google_id, role, status, created_at, updated_at) VALUES (#{loginId}, #{password}, #{name}, #{email}, - #{provider}, #{providerId}, #{role}, #{status}, NOW(), NOW()) + #{provider}, #{providerId}, #{googleId}, #{role}, #{status}, NOW(), NOW())